tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 252 253 See also: `SearchByTicker()`, `SearchInstruments()`. 254 """ 255 256 self.figi = "" 257 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 258 259 See also: `SearchByFIGI()`, `SearchInstruments()`. 260 """ 261 262 self.depth = 1 263 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 264 265 See also: `GetCurrentPrices()`. 266 """ 267 268 self.server = r"https://invest-public-api.tinkoff.ru/rest" 269 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 270 271 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 272 """ 273 274 uLogger.debug("Broker API server: {}".format(self.server)) 275 276 self.timeout = 15 277 """Server operations timeout in seconds. Default: `15`. 278 279 See also: `SendAPIRequest()`. 280 """ 281 282 self.headers = { 283 "Content-Type": "application/json", 284 "accept": "application/json", 285 "Authorization": "Bearer {}".format(self.token), 286 "x-app-name": "Tim55667757.TKSBrokerAPI", 287 } 288 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.body = None 294 """Request body which send to broker server. Default: `None`. 295 296 See also: `SendAPIRequest()`. 297 """ 298 299 self.historyFile = None 300 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 301 302 See also: `History()`. 303 """ 304 305 self.htmlHistoryFile = "index.html" 306 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 307 308 See also: `ShowHistoryChart()`. 309 """ 310 311 self.instrumentsFile = "instruments.md" 312 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 313 314 See also: `ShowInstrumentsInfo()`. 315 """ 316 317 self.searchResultsFile = "search-results.md" 318 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 319 320 See also: `SearchInstruments()`. 321 """ 322 323 self.pricesFile = "prices.md" 324 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 325 326 See also: `GetListOfPrices()`. 327 """ 328 329 self.infoFile = "info.md" 330 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 331 332 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 333 """ 334 335 self.bondsXLSXFile = "ext-bonds.xlsx" 336 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 337 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 338 339 See also: `ExtendBondsData()`. 340 """ 341 342 self.calendarFile = "calendar.md" 343 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 344 345 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 346 347 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 348 """ 349 350 self.overviewFile = "overview.md" 351 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 352 353 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 354 """ 355 356 self.overviewDigestFile = "overview-digest.md" 357 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 358 359 See also: `Overview()` with parameter `details="digest"`. 360 """ 361 362 self.overviewPositionsFile = "overview-positions.md" 363 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 364 365 See also: `Overview()` with parameter `details="positions"`. 366 """ 367 368 self.overviewOrdersFile = "overview-orders.md" 369 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 370 371 See also: `Overview()` with parameter `details="orders"`. 372 """ 373 374 self.overviewAnalyticsFile = "overview-analytics.md" 375 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 376 377 See also: `Overview()` with parameter `details="analytics"`. 378 """ 379 380 self.reportFile = "deals.md" 381 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 382 383 See also: `Deals()`. 384 """ 385 386 self.withdrawalLimitsFile = "limits.md" 387 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 388 389 See also: `OverviewLimits()` and `RequestLimits()`. 390 """ 391 392 self.userInfoFile = "user-info.md" 393 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 394 395 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 396 """ 397 398 self.userAccountsFile = "accounts.md" 399 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 400 401 See also: `OverviewAccounts()`, `RequestAccounts()`. 402 """ 403 404 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 405 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 406 407 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 408 409 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 410 """ 411 412 self.iList = None # init iList for raw instruments data 413 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 414 415 See also: `Listing()`, `DumpInstruments()`. 416 """ 417 418 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 419 if useCache: 420 if os.path.exists(self.iListDumpFile): 421 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 422 curTime = datetime.now(tzutc()) 423 424 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 425 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 426 427 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 428 429 else: 430 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 431 432 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 433 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 434 435 else: 436 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 437 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 438 439 else: 440 self.iList = self.Listing() # request new raw instruments data from broker server 441 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 442 443 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 444 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 445 446 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 447 """ 448 449 @staticmethod 450 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 451 """ 452 Parse JSON from response string. 453 454 :param rawData: this is a string with JSON-formatted text. 455 :param debug: if `True` then print more debug information. 456 :return: JSON (dictionary), parsed from server response string. 457 """ 458 if debug: 459 uLogger.debug("Raw text body:") 460 uLogger.debug(rawData) 461 462 responseJSON = json.loads(rawData) if rawData else {} 463 464 if debug: 465 uLogger.debug("JSON formatted:") 466 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 467 uLogger.debug(jsonLine) 468 469 return responseJSON 470 471 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 472 """ 473 Send GET or POST request to broker server and receive JSON object. 474 475 self.header: must be defining with dictionary of headers. 476 self.body: if define then used as request body. None by default. 477 self.timeout: global request timeout, 15 seconds by default. 478 :param url: url with REST request. 479 :param reqType: send "GET" or "POST" request. "GET" by default. 480 :param retry: how many times retry after first request if an 5xx server errors occurred. 481 :param pause: sleep time in seconds between retries. 482 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 483 :return: response JSON (dictionary) from broker. 484 """ 485 if reqType not in ("GET", "POST"): 486 uLogger.error("You can define request type: 'GET' or 'POST'!") 487 raise Exception("Incorrect value") 488 489 if debug: 490 uLogger.debug("Request parameters:") 491 uLogger.debug(" - REST API URL: {}".format(url)) 492 uLogger.debug(" - request type: {}".format(reqType)) 493 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 494 uLogger.debug(" - body: {}".format(self.body)) 495 496 # fast hack to avoid all operations with some tickers/FIGI 497 responseJSON = {} 498 oK = True 499 for item in self.exclude: 500 if item in url: 501 if debug: 502 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 503 504 oK = False 505 break 506 507 if oK: 508 counter = 0 509 response = None 510 errMsg = "" 511 512 while not response and counter <= retry: 513 if reqType == "GET": 514 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if reqType == "POST": 517 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 518 519 if debug: 520 uLogger.debug("Response:") 521 uLogger.debug(" - status code: {}".format(response.status_code)) 522 uLogger.debug(" - reason: {}".format(response.reason)) 523 uLogger.debug(" - body length: {}".format(len(response.text))) 524 uLogger.debug(" - headers: {}".format(response.headers)) 525 526 # Server returns some headers: 527 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 528 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 529 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 530 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 531 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 532 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 533 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 534 sleep(rateLimitWait) 535 536 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 537 if 400 <= response.status_code < 500: 538 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 539 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 540 counter = retry + 1 541 542 if 500 <= response.status_code < 600: 543 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 544 uLogger.debug(" - not oK, {}".format(errMsg)) 545 counter += 1 546 547 if counter <= retry: 548 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 549 sleep(pause) 550 551 responseJSON = self._ParseJSON(response.text) 552 553 if errMsg: 554 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 555 uLogger.error(" - not oK, {}".format(errMsg)) 556 557 return responseJSON 558 559 def _IUpdater(self, iType: str) -> tuple: 560 """ 561 Request instrument by type from server. See available API methods for instruments: 562 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 563 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 564 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 565 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 566 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 567 568 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 569 :return: tuple with iType name and list of available instruments of current type for defined user token. 570 """ 571 result = [] 572 573 if iType in TKS_INSTRUMENTS: 574 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 575 576 # all instruments have the same body in API v2 requests: 577 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 578 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 579 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 580 581 return iType, result 582 583 def _IWrapper(self, kwargs): 584 """ 585 Wrapper runs instrument's update method `_IUpdater()`. 586 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 587 """ 588 return self._IUpdater(**kwargs) 589 590 def Listing(self) -> dict: 591 """ 592 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 593 594 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 595 """ 596 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 597 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 598 599 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 600 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 601 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 602 603 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 604 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 605 poolUpdater.close() 606 607 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 608 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 609 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 610 611 # calculate minimum price increment (step) for all instruments and set up instrument's type: 612 for iType in iList.keys(): 613 for ticker in iList[iType]: 614 iList[iType][ticker]["type"] = iType 615 616 if "minPriceIncrement" in iList[iType][ticker].keys(): 617 iList[iType][ticker]["step"] = NanoToFloat( 618 iList[iType][ticker]["minPriceIncrement"]["units"], 619 iList[iType][ticker]["minPriceIncrement"]["nano"], 620 ) 621 622 else: 623 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 624 625 return iList 626 627 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 628 """ 629 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 630 631 See also: `DumpInstruments()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 635 """ 636 if self.iListDumpFile is None or not self.iListDumpFile: 637 uLogger.error("Output name of dump file must be defined!") 638 raise Exception("Filename required") 639 640 if not self.iList or forceUpdate: 641 self.iList = self.Listing() 642 643 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 644 645 # Save as XLSX with separated sheets for every type of instruments: 646 with pd.ExcelWriter( 647 path=xlsxDumpFile, 648 date_format=TKS_DATE_FORMAT, 649 datetime_format=TKS_DATE_TIME_FORMAT, 650 mode="w", 651 ) as writer: 652 for iType in TKS_INSTRUMENTS: 653 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 654 df = df[sorted(df)] # sorted by column names 655 df = df.applymap( 656 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 657 na_action="ignore", 658 ) # converting numbers from nano-type to float in every cell 659 df.to_excel( 660 writer, 661 sheet_name=iType, 662 encoding="UTF-8", 663 freeze_panes=(1, 1), 664 ) # saving as XLSX-file with freeze first row and column as headers 665 666 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 667 668 def DumpInstruments(self, forceUpdate: bool = True) -> str: 669 """ 670 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 671 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 672 673 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 674 675 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 676 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 677 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 678 """ 679 if self.iListDumpFile is None or not self.iListDumpFile: 680 uLogger.error("Output name of dump file must be defined!") 681 raise Exception("Filename required") 682 683 if not self.iList or forceUpdate: 684 self.iList = self.Listing() 685 686 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 687 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 688 fH.write(jsonDump) 689 690 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 691 692 return jsonDump 693 694 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 695 """ 696 Show information about one instrument defined by json data and prints it in Markdown format. 697 698 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 699 700 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 701 :param show: if `True` then also printing information about instrument and its current price. 702 :return: multilines text in Markdown format with information about one instrument. 703 """ 704 splitLine = "| | |\n" 705 infoText = "" 706 707 if iJSON is not None and iJSON and isinstance(iJSON, dict): 708 info = [ 709 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 710 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 711 "| Parameters | Values |\n", 712 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 713 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 714 "| Full name: | {:<54} |\n".format(iJSON["name"]), 715 ] 716 717 if "sector" in iJSON.keys() and iJSON["sector"]: 718 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 719 720 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 721 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 722 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 723 ))) 724 725 info.extend([ 726 splitLine, 727 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 728 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 729 ]) 730 731 if "isin" in iJSON.keys() and iJSON["isin"]: 732 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 733 734 if "classCode" in iJSON.keys(): 735 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 736 737 info.extend([ 738 splitLine, 739 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 740 splitLine, 741 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 742 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 743 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 744 ]) 745 746 if iJSON["figi"]: 747 self.figi = iJSON["figi"] 748 iJSON = iJSON | self.RequestTradingStatus() 749 750 info.extend([ 751 splitLine, 752 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 753 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 754 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 755 ]) 756 757 info.append(splitLine) 758 759 if "type" in iJSON.keys() and iJSON["type"]: 760 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 761 762 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 763 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 764 765 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 766 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 767 768 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 769 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 770 771 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 772 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 773 774 if "focusType" in iJSON.keys() and iJSON["focusType"]: 775 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 776 777 if "assetType" in iJSON.keys() and iJSON["assetType"]: 778 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 779 780 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 781 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 782 783 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 784 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 785 786 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 787 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 788 789 if "currency" in iJSON.keys(): 790 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 791 792 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 793 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 794 795 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 796 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 799 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 800 801 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 802 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 803 804 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 805 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 806 807 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 808 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 809 810 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 811 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 812 813 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 814 info.append("| Perpetual bond: | Yes |\n") 815 816 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 817 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 818 819 iExt = None 820 if iJSON["type"] == "Bonds": 821 info.extend([ 822 splitLine, 823 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 824 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 825 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 826 iJSON["nominal"]["currency"], 827 )), 828 ]) 829 830 if "floatingCouponFlag" in iJSON.keys(): 831 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 832 833 if "amortizationFlag" in iJSON.keys(): 834 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 835 836 info.append(splitLine) 837 838 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 839 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 840 841 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 842 843 info.extend([ 844 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 845 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 846 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 847 ]) 848 849 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 850 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 851 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 852 iJSON["aciValue"]["currency"] 853 ))) 854 855 if "currentPrice" in iJSON.keys(): 856 info.append(splitLine) 857 858 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 859 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 860 861 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 862 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 863 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 864 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 865 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 866 867 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 868 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 869 870 info.extend([ 871 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 872 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 873 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 874 )), 875 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 876 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 877 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 878 )), 879 "| Changes between last deal price and last close | {:<54} |\n".format( 880 "{:.2f}%{}".format( 881 iJSON["currentPrice"]["changes"], 882 " ({}{:.2f} {})".format( 883 "+" if bondChangesDelta > 0 else "", 884 bondChangesDelta, 885 aciCurrency 886 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 887 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 888 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 889 currency 890 ), 891 ) 892 ), 893 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 894 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 897 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 898 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 899 )), 900 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 901 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 904 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 905 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 906 )), 907 ]) 908 909 if "lot" in iJSON.keys(): 910 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 911 912 if "step" in iJSON.keys() and iJSON["step"] != 0: 913 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 914 915 # Add bond payment calendar: 916 if iJSON["type"] == "Bonds": 917 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 918 info.extend(["\n", strCalendar]) 919 920 infoText += "".join(info) 921 922 if show: 923 uLogger.info("{}".format(infoText)) 924 925 else: 926 uLogger.debug("{}".format(infoText)) 927 928 if self.infoFile is not None: 929 with open(self.infoFile, "w", encoding="UTF-8") as fH: 930 fH.write(infoText) 931 932 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 933 934 return infoText 935 936 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 937 """ 938 Search and return raw broker's information about instrument by its ticker. 939 `ticker` must be defined! If debug=True then print all debug messages. 940 941 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 942 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 943 :param debug: if `True` then print all debug console messages. 944 :return: JSON formatted data with information about instrument. 945 """ 946 tickerJSON = {} 947 if debug: 948 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 949 950 if not self.ticker: 951 uLogger.warning("self.ticker variable is not be empty!") 952 953 else: 954 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 955 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 956 raise Exception("Instrument not allowed") 957 958 if not self.iList: 959 self.iList = self.Listing() 960 961 if self.ticker in self.iList["Shares"].keys(): 962 tickerJSON = self.iList["Shares"][self.ticker] 963 if debug: 964 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 965 966 elif self.ticker in self.iList["Currencies"].keys(): 967 tickerJSON = self.iList["Currencies"][self.ticker] 968 if debug: 969 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Bonds"].keys(): 972 tickerJSON = self.iList["Bonds"][self.ticker] 973 if debug: 974 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Etfs"].keys(): 977 tickerJSON = self.iList["Etfs"][self.ticker] 978 if debug: 979 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 980 981 elif self.ticker in self.iList["Futures"].keys(): 982 tickerJSON = self.iList["Futures"][self.ticker] 983 if debug: 984 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 985 986 if tickerJSON: 987 self.figi = tickerJSON["figi"] 988 989 if requestPrice: 990 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 991 992 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 993 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 994 995 else: 996 tickerJSON["currentPrice"]["changes"] = 0 997 998 if show: 999 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1000 1001 else: 1002 if show: 1003 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1004 1005 return tickerJSON 1006 1007 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1008 """ 1009 Search and return raw broker's information about instrument by its FIGI. 1010 `figi` must be defined! If debug=True then print all debug messages. 1011 1012 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1013 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1014 :param debug: if `True` then print all debug console messages. 1015 :return: JSON formatted data with information about instrument. 1016 """ 1017 figiJSON = {} 1018 if debug: 1019 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1020 1021 if not self.figi: 1022 uLogger.warning("self.figi variable is not be empty!") 1023 1024 else: 1025 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1026 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1027 raise Exception("Instrument not allowed") 1028 1029 if not self.iList: 1030 self.iList = self.Listing() 1031 1032 for item in self.iList["Shares"].keys(): 1033 if self.figi == self.iList["Shares"][item]["figi"]: 1034 figiJSON = self.iList["Shares"][item] 1035 1036 if debug: 1037 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1038 1039 break 1040 1041 if not figiJSON: 1042 for item in self.iList["Currencies"].keys(): 1043 if self.figi == self.iList["Currencies"][item]["figi"]: 1044 figiJSON = self.iList["Currencies"][item] 1045 1046 if debug: 1047 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1048 1049 break 1050 1051 if not figiJSON: 1052 for item in self.iList["Bonds"].keys(): 1053 if self.figi == self.iList["Bonds"][item]["figi"]: 1054 figiJSON = self.iList["Bonds"][item] 1055 1056 if debug: 1057 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1058 1059 break 1060 1061 if not figiJSON: 1062 for item in self.iList["Etfs"].keys(): 1063 if self.figi == self.iList["Etfs"][item]["figi"]: 1064 figiJSON = self.iList["Etfs"][item] 1065 1066 if debug: 1067 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1068 1069 break 1070 1071 if not figiJSON: 1072 for item in self.iList["Futures"].keys(): 1073 if self.figi == self.iList["Futures"][item]["figi"]: 1074 figiJSON = self.iList["Futures"][item] 1075 1076 if debug: 1077 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1078 1079 break 1080 1081 if figiJSON: 1082 self.figi = figiJSON["figi"] 1083 self.ticker = figiJSON["ticker"] 1084 1085 if requestPrice: 1086 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1087 1088 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1089 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1090 1091 else: 1092 figiJSON["currentPrice"]["changes"] = 0 1093 1094 if show: 1095 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1096 1097 else: 1098 if show: 1099 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1100 1101 return figiJSON 1102 1103 def GetCurrentPrices(self, show: bool = True) -> dict: 1104 """ 1105 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1106 `{"buy": [{"price": 1243.8, "quantity": 193}, 1107 {"price": 1244.0, "quantity": 168}, 1108 {"price": 1244.8, "quantity": 5}, 1109 {"price": 1245.0, "quantity": 61}, 1110 {"price": 1245.4, "quantity": 60}], 1111 "sell": [{"price": 1243.6, "quantity": 8}, 1112 {"price": 1242.6, "quantity": 10}, 1113 {"price": 1242.4, "quantity": 18}, 1114 {"price": 1242.2, "quantity": 50}, 1115 {"price": 1242.0, "quantity": 113}], 1116 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1117 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1118 - sell: list of dicts with Buyers prices, 1119 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1120 - quantity: volume value by current price in lots, 1121 - limitUp: current trade session limit price, maximum, 1122 - limitDown: current trade session limit price, minimum, 1123 - lastPrice: last deal price of the instrument, 1124 - closePrice: previous trade session close price of the instrument. 1125 1126 See also: `SearchByTicker()` and `SearchByFIGI()`. 1127 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1128 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1129 1130 :param show: if `True` then print DOM to log and console. 1131 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1132 If an error occurred then returns an empty record: 1133 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1134 """ 1135 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1136 1137 if self.depth < 1: 1138 uLogger.error("Depth of Market (DOM) must be >=1!") 1139 raise Exception("Incorrect value") 1140 1141 if not (self.ticker or self.figi): 1142 uLogger.error("self.ticker or self.figi variables must be defined!") 1143 raise Exception("Ticker or FIGI required") 1144 1145 if self.ticker and not self.figi: 1146 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1147 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1148 1149 if not self.ticker and self.figi: 1150 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1151 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1152 1153 if not self.figi: 1154 uLogger.error("FIGI is not defined!") 1155 raise Exception("Ticker or FIGI required") 1156 1157 else: 1158 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1159 1160 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1161 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1162 self.body = str({"figi": self.figi, "depth": self.depth}) 1163 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1164 1165 if pricesResponse: 1166 # list of dicts with sellers orders: 1167 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1168 1169 # list of dicts with buyers orders: 1170 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1171 1172 # max price of instrument at this time: 1173 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1174 1175 # min price of instrument at this time: 1176 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1177 1178 # last price of deal with instrument: 1179 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1180 1181 # last close price of instrument: 1182 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1183 1184 else: 1185 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1186 uLogger.debug("Server response: {}".format(pricesResponse)) 1187 1188 if show: 1189 if prices["buy"] or prices["sell"]: 1190 info = [ 1191 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1192 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1193 self.ticker, 1194 self.figi, 1195 self.depth, 1196 ), 1197 "-" * 60, "\n", 1198 " Orders of Buyers | Orders of Sellers\n", 1199 "-" * 60, "\n", 1200 " Sell prices (volumes) | Buy prices (volumes)\n", 1201 "-" * 60, "\n", 1202 ] 1203 1204 if not prices["buy"]: 1205 info.append(" | No orders!\n") 1206 sumBuy = 0 1207 1208 else: 1209 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1210 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1211 for item in maxMinSorted: 1212 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1213 1214 if not prices["sell"]: 1215 info.append("No orders! |\n") 1216 sumSell = 0 1217 1218 else: 1219 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1220 for item in prices["sell"]: 1221 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1222 1223 info.extend([ 1224 "-" * 60, "\n", 1225 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1226 "-" * 60, "\n", 1227 ]) 1228 1229 infoText = "".join(info) 1230 1231 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1232 1233 else: 1234 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1235 1236 return prices 1237 1238 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1239 """ 1240 This method get and show information about all available broker instruments for current user account. 1241 If `instrumentsFile` string is not empty then also save information to this file. 1242 1243 :param show: if `True` then print results to console, if `False` - print only to file. 1244 :return: multi-lines string with all available broker instruments 1245 """ 1246 if not self.iList: 1247 self.iList = self.Listing() 1248 1249 info = [ 1250 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1251 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1252 ] 1253 1254 # add instruments count by type: 1255 for iType in self.iList.keys(): 1256 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1257 1258 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1259 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1260 1261 # generating info tables with all instruments by type: 1262 for iType in self.iList.keys(): 1263 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1264 1265 for instrument in self.iList[iType].keys(): 1266 iName = self.iList[iType][instrument]["name"] # instrument's name 1267 if len(iName) > 57: 1268 iName = "{}...".format(iName[:54]) # right trim for a long string 1269 1270 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1271 self.iList[iType][instrument]["ticker"], 1272 iName, 1273 self.iList[iType][instrument]["figi"], 1274 self.iList[iType][instrument]["currency"], 1275 self.iList[iType][instrument]["lot"], 1276 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1277 )) 1278 1279 infoText = "".join(info) 1280 1281 if show: 1282 uLogger.info(infoText) 1283 1284 if self.instrumentsFile: 1285 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1286 fH.write(infoText) 1287 1288 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1289 1290 return infoText 1291 1292 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1293 """ 1294 This method search and show information about instruments by part of its ticker, FIGI or name. 1295 If `searchResultsFile` string is not empty then also save information to this file. 1296 1297 :param pattern: string with part of ticker, FIGI or instrument's name. 1298 :param show: if `True` then print results to console, if `False` - return list of result only. 1299 :return: list of dictionaries with all found instruments. 1300 """ 1301 if not self.iList: 1302 self.iList = self.Listing() 1303 1304 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1305 compiledPattern = re.compile(pattern, re.IGNORECASE) 1306 1307 for iType in self.iList: 1308 for instrument in self.iList[iType].values(): 1309 searchResult = compiledPattern.search(" ".join( 1310 [instrument["ticker"], instrument["figi"], instrument["name"]] 1311 )) 1312 1313 if searchResult: 1314 searchResults[iType][instrument["ticker"]] = instrument 1315 1316 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1317 info = [ 1318 "# Search results\n\n", 1319 "* **Search pattern:** [{}]\n".format(pattern), 1320 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1321 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1322 ] 1323 infoShort = info[:] 1324 1325 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1326 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1327 skippedLine = "| ... | ... | ... | ... |\n" 1328 1329 if resultsLen == 0: 1330 info.append("\nNo results\n") 1331 infoShort.append("\nNo results\n") 1332 uLogger.warning("No results. Try changing your search pattern.") 1333 1334 else: 1335 for iType in searchResults: 1336 iTypeValuesCount = len(searchResults[iType].values()) 1337 if iTypeValuesCount > 0: 1338 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 1341 for instrument in searchResults[iType].values(): 1342 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1343 instrument["type"], 1344 instrument["ticker"], 1345 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1346 instrument["figi"], 1347 )) 1348 1349 if iTypeValuesCount <= 5: 1350 infoShort.extend(info[-iTypeValuesCount:]) 1351 1352 else: 1353 infoShort.extend(info[-5:]) 1354 infoShort.append(skippedLine) 1355 1356 infoText = "".join(info) 1357 infoTextShort = "".join(infoShort) 1358 1359 if show: 1360 uLogger.info(infoTextShort) 1361 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1362 1363 if self.searchResultsFile: 1364 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1365 fH.write(infoText) 1366 1367 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1368 1369 return searchResults 1370 1371 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1372 """ 1373 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1374 1375 :param instruments: list of strings with tickers or FIGIs. 1376 :return: list with unique instrument FIGIs only. 1377 """ 1378 requestedInstruments = [] 1379 for iName in instruments: 1380 if iName not in self.aliases.keys(): 1381 if iName not in requestedInstruments: 1382 requestedInstruments.append(iName) 1383 1384 else: 1385 if iName not in requestedInstruments: 1386 if self.aliases[iName] not in requestedInstruments: 1387 requestedInstruments.append(self.aliases[iName]) 1388 1389 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1390 1391 onlyUniqueFIGIs = [] 1392 for iName in requestedInstruments: 1393 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1394 continue 1395 1396 self.ticker = iName 1397 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1398 1399 if not iData: 1400 self.ticker = "" 1401 self.figi = iName 1402 1403 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1404 1405 if not iData: 1406 self.figi = "" 1407 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1408 1409 if iData and iData["figi"] not in onlyUniqueFIGIs: 1410 onlyUniqueFIGIs.append(iData["figi"]) 1411 1412 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1413 1414 return onlyUniqueFIGIs 1415 1416 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1417 """ 1418 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1419 See limits: https://tinkoff.github.io/investAPI/limits/ 1420 If `pricesFile` string is not empty then also save information to this file. 1421 1422 :param instruments: list of strings with tickers or FIGIs. 1423 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1424 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1425 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1426 """ 1427 if instruments is None or not instruments: 1428 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1429 raise Exception("Ticker or FIGI required") 1430 1431 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1432 1433 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1434 1435 iList = [] # trying to get info and current prices about all unique instruments: 1436 for self.figi in onlyUniqueFIGIs: 1437 iData = self.SearchByFIGI(requestPrice=True) 1438 iList.append(iData) 1439 1440 self.ShowListOfPrices(iList, show) 1441 1442 return iList 1443 1444 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1445 """ 1446 Show table contains current prices of given instruments. 1447 1448 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1449 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1450 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1451 :return: multilines text in Markdown format as a table contains current prices. 1452 """ 1453 infoText = "" 1454 1455 if show or self.pricesFile: 1456 info = [ 1457 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1458 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1459 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1460 ] 1461 1462 for item in iList: 1463 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1464 item["ticker"], 1465 item["figi"], 1466 item["type"], 1467 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1468 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1469 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1470 "{} / {}".format( 1471 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1472 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1473 ), 1474 "{} / {}".format( 1475 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1476 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1477 ), 1478 item["currency"], 1479 )) 1480 1481 infoText = "".join(info) 1482 1483 if show: 1484 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1485 1486 if self.pricesFile: 1487 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1488 fH.write(infoText) 1489 1490 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1491 1492 return infoText 1493 1494 def RequestTradingStatus(self) -> dict: 1495 """ 1496 Requesting trading status for the instrument defined by `figi` variable. 1497 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1498 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1499 1500 :return: dictionary with trading status attributes. Response example: 1501 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1502 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1503 """ 1504 if self.figi is None or not self.figi: 1505 uLogger.error("Variable `figi` must be defined for using this method!") 1506 raise Exception("FIGI required") 1507 1508 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1509 1510 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1511 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1512 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1513 1514 uLogger.debug("Records about current trading status successfully received") 1515 1516 return tradingStatus 1517 1518 def RequestPortfolio(self) -> dict: 1519 """ 1520 Requesting actual user's portfolio for current `accountId`. 1521 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1522 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1523 1524 :return: dictionary with user's portfolio. 1525 """ 1526 if self.accountId is None or not self.accountId: 1527 uLogger.error("Variable `accountId` must be defined for using this method!") 1528 raise Exception("Account ID required") 1529 1530 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1531 1532 self.body = str({"accountId": self.accountId}) 1533 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1534 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1535 1536 uLogger.debug("Records about user's portfolio successfully received") 1537 1538 return rawPortfolio 1539 1540 def RequestPositions(self) -> dict: 1541 """ 1542 Requesting open positions by currencies and instruments for current `accountId`. 1543 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1544 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1545 1546 :return: dictionary with open positions by instruments. 1547 """ 1548 if self.accountId is None or not self.accountId: 1549 uLogger.error("Variable `accountId` must be defined for using this method!") 1550 raise Exception("Account ID required") 1551 1552 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1553 1554 self.body = str({"accountId": self.accountId}) 1555 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1556 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1557 1558 uLogger.debug("Records about current open positions successfully received") 1559 1560 return rawPositions 1561 1562 def RequestPendingOrders(self) -> list: 1563 """ 1564 Requesting current actual pending orders for current `accountId`. 1565 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1566 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1567 1568 :return: list of dictionaries with pending orders. 1569 """ 1570 if self.accountId is None or not self.accountId: 1571 uLogger.error("Variable `accountId` must be defined for using this method!") 1572 raise Exception("Account ID required") 1573 1574 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1575 1576 self.body = str({"accountId": self.accountId}) 1577 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1578 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1579 1580 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1581 1582 return rawOrders 1583 1584 def RequestStopOrders(self) -> list: 1585 """ 1586 Requesting current actual stop orders for current `accountId`. 1587 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1588 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1589 1590 :return: list of dictionaries with stop orders. 1591 """ 1592 if self.accountId is None or not self.accountId: 1593 uLogger.error("Variable `accountId` must be defined for using this method!") 1594 raise Exception("Account ID required") 1595 1596 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1597 1598 self.body = str({"accountId": self.accountId}) 1599 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1600 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1601 1602 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1603 1604 return rawStopOrders 1605 1606 def Overview(self, show: bool = False, details: str = "full") -> dict: 1607 """ 1608 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1609 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1610 are defined then also save information to file. 1611 1612 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1613 many requests about the state of the portfolio, and then, based on the received data, a large number 1614 of calculation and statistics are collected. 1615 1616 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1617 :param details: how detailed should the information be? You should specify one of strings: 1618 `full` - shows full available information about portfolio status (by default), 1619 `positions` - shows only open positions, 1620 `digest` - show a short digest of the portfolio status, 1621 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1622 `orders` - shows only sections of open limits and stop orders. 1623 :return: dictionary with client's raw portfolio and some statistics. 1624 """ 1625 if self.accountId is None or not self.accountId: 1626 uLogger.error("Variable `accountId` must be defined for using this method!") 1627 raise Exception("Account ID required") 1628 1629 view = { 1630 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1631 "headers": {}, # list of dictionaries, response headers without "positions" section 1632 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1633 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1634 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1635 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1636 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1637 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1638 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1639 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1640 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1641 }, 1642 "stat": { # --- some statistics calculated using "raw" sections: 1643 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1644 "availableRUB": 0., # available rubles (without other currencies) 1645 "blockedRUB": 0., # blocked sum in Russian Rouble 1646 "totalChangesRUB": 0., # changes for all open trades in RUB 1647 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1648 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1649 "sharesCostRUB": 0., # costs of all shares in RUB 1650 "bondsCostRUB": 0., # costs of all bonds in RUB 1651 "etfsCostRUB": 0., # costs of all etfs in RUB 1652 "futuresCostRUB": 0., # costs of all futures in RUB 1653 "Currencies": [], # list of dictionaries of all currencies statistics 1654 "Shares": [], # list of dictionaries of all shares statistics 1655 "Bonds": [], # list of dictionaries of all bonds statistics 1656 "Etfs": [], # list of dictionaries of all etfs statistics 1657 "Futures": [], # list of dictionaries of all futures statistics 1658 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1659 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1660 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1661 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1662 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1663 }, 1664 "analytics": { # --- some analytics of portfolio: 1665 "distrByAssets": {}, # portfolio distribution by assets 1666 "distrByCompanies": {}, # portfolio distribution by companies 1667 "distrBySectors": {}, # portfolio distribution by sectors 1668 "distrByCurrencies": {}, # portfolio distribution by currencies 1669 "distrByCountries": {}, # portfolio distribution by countries 1670 } 1671 } 1672 1673 details = details.lower() 1674 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1675 if details not in availableDetails: 1676 details = "full" 1677 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1678 1679 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1680 1681 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1682 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1683 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1684 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1685 1686 # save response headers without "positions" section: 1687 for key in portfolioResponse.keys(): 1688 if key != "positions": 1689 view["raw"]["headers"][key] = portfolioResponse[key] 1690 1691 else: 1692 continue 1693 1694 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1695 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1696 for item in portfolioResponse["positions"]: 1697 if item["instrumentType"] == "currency": 1698 self.figi = item["figi"] 1699 curr = self.SearchByFIGI(requestPrice=False) 1700 1701 # current price of currency in RUB: 1702 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1703 "name": curr["name"], 1704 "currentPrice": NanoToFloat( 1705 item["currentPrice"]["units"], 1706 item["currentPrice"]["nano"] 1707 ), 1708 } 1709 1710 view["raw"]["Currencies"].append(item) 1711 1712 elif item["instrumentType"] == "share": 1713 view["raw"]["Shares"].append(item) 1714 1715 elif item["instrumentType"] == "bond": 1716 view["raw"]["Bonds"].append(item) 1717 1718 elif item["instrumentType"] == "etf": 1719 view["raw"]["Etfs"].append(item) 1720 1721 elif item["instrumentType"] == "futures": 1722 view["raw"]["Futures"].append(item) 1723 1724 else: 1725 continue 1726 1727 # how many volume of currencies (by ISO currency name) are blocked: 1728 for item in view["raw"]["positions"]["blocked"]: 1729 blocked = NanoToFloat(item["units"], item["nano"]) 1730 if blocked > 0: 1731 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1732 1733 # how many volume of instruments (by FIGI) are blocked: 1734 for item in view["raw"]["positions"]["securities"]: 1735 blocked = int(item["blocked"]) 1736 if blocked > 0: 1737 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1738 1739 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1740 1741 if "rub" in allBlocked.keys(): 1742 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1743 1744 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1745 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1746 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1747 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1748 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1749 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1750 view["stat"]["portfolioCostRUB"] = sum([ 1751 view["stat"]["allCurrenciesCostRUB"], 1752 view["stat"]["sharesCostRUB"], 1753 view["stat"]["bondsCostRUB"], 1754 view["stat"]["etfsCostRUB"], 1755 view["stat"]["futuresCostRUB"], 1756 ]) 1757 1758 # --- calculating some portfolio statistics: 1759 byComp = {} # distribution by companies 1760 bySect = {} # distribution by sectors 1761 byCurr = {} # distribution by currencies (include RUB) 1762 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1763 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1764 1765 for item in portfolioResponse["positions"]: 1766 self.figi = item["figi"] 1767 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1768 1769 if instrument: 1770 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1771 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1772 1773 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1774 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1775 1776 else: 1777 blocked = 0 1778 1779 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1780 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1781 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1782 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1783 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1784 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1785 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1786 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1787 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1788 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1789 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1790 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1791 1792 statData = { 1793 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1794 "ticker": instrument["ticker"], # ticker by FIGI 1795 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1796 "volume": volume, # available volume of instrument 1797 "lots": lots, # volume in lots of instrument 1798 "direction": direction, # direction of an instrument's position: short or long 1799 "blocked": blocked, # blocked volume of currency or instrument 1800 "currentPrice": curPrice, # current instrument's price in basic asset 1801 "average": average, # current average position price 1802 "cost": cost, # current cost of all volume of instrument in basic asset 1803 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1804 "costRUB": costRUB, # cost of instrument in ruble 1805 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1806 "profit": profit, # expected profit at current moment 1807 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1808 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1809 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1810 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1811 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1812 "step": instrument["step"], # minimum price increment 1813 } 1814 1815 # adding distribution by unique countries: 1816 if statData["country"] not in byCountry.keys(): 1817 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1818 1819 else: 1820 byCountry[statData["country"]]["cost"] += costRUB 1821 byCountry[statData["country"]]["percent"] += percentCostRUB 1822 1823 if item["instrumentType"] != "currency": 1824 # adding distribution by unique companies: 1825 if statData["name"]: 1826 if statData["name"] not in byComp.keys(): 1827 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1828 1829 else: 1830 byComp[statData["name"]]["cost"] += costRUB 1831 byComp[statData["name"]]["percent"] += percentCostRUB 1832 1833 # adding distribution by unique sectors: 1834 if statData["sector"] not in bySect.keys(): 1835 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1836 1837 else: 1838 bySect[statData["sector"]]["cost"] += costRUB 1839 bySect[statData["sector"]]["percent"] += percentCostRUB 1840 1841 # adding distribution by unique currencies: 1842 if currency not in byCurr.keys(): 1843 byCurr[currency] = { 1844 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1845 "cost": costRUB, 1846 "percent": percentCostRUB 1847 } 1848 1849 else: 1850 byCurr[currency]["cost"] += costRUB 1851 byCurr[currency]["percent"] += percentCostRUB 1852 1853 # saving statistics for every instrument: 1854 if item["instrumentType"] == "currency": 1855 view["stat"]["Currencies"].append(statData) 1856 1857 # update dict with free funds for trading (total - blocked) by currencies 1858 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1859 view["stat"]["funds"][currency] = { 1860 "total": volume, 1861 "totalCostRUB": costRUB, # total volume cost in rubles 1862 "free": volume - blocked, 1863 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1864 } 1865 1866 elif item["instrumentType"] == "share": 1867 view["stat"]["Shares"].append(statData) 1868 1869 elif item["instrumentType"] == "bond": 1870 view["stat"]["Bonds"].append(statData) 1871 1872 elif item["instrumentType"] == "etf": 1873 view["stat"]["Etfs"].append(statData) 1874 1875 elif item["instrumentType"] == "Futures": 1876 view["stat"]["Futures"].append(statData) 1877 1878 else: 1879 continue 1880 1881 # total changes in Russian Ruble: 1882 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1883 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1884 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1885 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1886 view["stat"]["funds"]["rub"] = { 1887 "total": view["stat"]["availableRUB"], 1888 "totalCostRUB": view["stat"]["availableRUB"], 1889 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1890 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 } 1892 1893 # --- pending orders sector data: 1894 uniquePendingOrders = [] 1895 uniquePendingOrdersFIGIs = [] 1896 for item in view["raw"]["orders"]: 1897 if item["figi"] not in uniquePendingOrdersFIGIs: 1898 uniquePendingOrdersFIGIs.append(item["figi"]) 1899 uniquePendingOrders.append(item) 1900 1901 for item in uniquePendingOrders: 1902 self.figi = item["figi"] 1903 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1904 1905 if instrument: 1906 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1907 orderType = TKS_ORDER_TYPES[item["orderType"]] 1908 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1909 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1910 1911 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1912 if item["direction"] == "ORDER_DIRECTION_BUY": 1913 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1914 1915 else: 1916 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1917 1918 # requested price for order execution: 1919 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1920 1921 # necessary changes in percent to reach target from current price: 1922 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1923 1924 view["stat"]["orders"].append({ 1925 "orderID": item["orderId"], # orderId number parameter of current order 1926 "figi": item["figi"], # FIGI identification 1927 "ticker": instrument["ticker"], # ticker name by FIGI 1928 "lotsRequested": item["lotsRequested"], # requested lots value 1929 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1930 "currentPrice": lastPrice, # current instrument's price for defined action 1931 "targetPrice": target, # requested price for order execution in base currency 1932 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1933 "percentChanges": changes, # changes in percent to target from current price 1934 "currency": item["currency"], # instrument's currency name 1935 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1936 "type": orderType, # type of order from TKS_ORDER_TYPES 1937 "status": orderState, # order status from TKS_ORDER_STATES 1938 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1939 }) 1940 1941 # --- stop orders sector data: 1942 uniqueStopOrders = [] 1943 uniqueStopOrdersFIGIs = [] 1944 for item in view["raw"]["stopOrders"]: 1945 if item["figi"] not in uniqueStopOrdersFIGIs: 1946 uniqueStopOrdersFIGIs.append(item["figi"]) 1947 uniqueStopOrders.append(item) 1948 1949 for item in uniqueStopOrders: 1950 self.figi = item["figi"] 1951 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1952 1953 if instrument: 1954 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1955 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1956 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1957 1958 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1959 if "expirationTime" in item.keys(): 1960 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1961 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1962 1963 else: 1964 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1965 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1966 1967 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1968 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1969 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1970 1971 else: 1972 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1973 1974 # requested price when stop-order executed: 1975 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1976 1977 # price for limit-order, set up when stop-order executed: 1978 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1979 1980 # necessary changes in percent to reach target from current price: 1981 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1982 1983 view["stat"]["stopOrders"].append({ 1984 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1985 "figi": item["figi"], # FIGI identification 1986 "ticker": instrument["ticker"], # ticker name by FIGI 1987 "lotsRequested": item["lotsRequested"], # requested lots value 1988 "currentPrice": lastPrice, # current instrument's price for defined action 1989 "targetPrice": target, # requested price for stop-order execution in base currency 1990 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1991 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1992 "percentChanges": changes, # changes in percent to target from current price 1993 "currency": item["currency"], # instrument's currency name 1994 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1995 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1996 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1997 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1998 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1999 }) 2000 2001 # --- calculating data for analytics section: 2002 # portfolio distribution by assets: 2003 view["analytics"]["distrByAssets"] = { 2004 "Ruble": { 2005 "uniques": 1, 2006 "cost": view["stat"]["availableRUB"], 2007 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2008 }, 2009 "Currencies": { 2010 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2011 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2012 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 }, 2014 "Shares": { 2015 "uniques": len(view["stat"]["Shares"]), 2016 "cost": view["stat"]["sharesCostRUB"], 2017 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 }, 2019 "Bonds": { 2020 "uniques": len(view["stat"]["Bonds"]), 2021 "cost": view["stat"]["bondsCostRUB"], 2022 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2023 }, 2024 "Etfs": { 2025 "uniques": len(view["stat"]["Etfs"]), 2026 "cost": view["stat"]["etfsCostRUB"], 2027 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2028 }, 2029 "Futures": { 2030 "uniques": len(view["stat"]["Futures"]), 2031 "cost": view["stat"]["futuresCostRUB"], 2032 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 } 2035 2036 # portfolio distribution by companies: 2037 view["analytics"]["distrByCompanies"]["All money cash"] = { 2038 "ticker": "", 2039 "cost": view["stat"]["allCurrenciesCostRUB"], 2040 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 } 2042 view["analytics"]["distrByCompanies"].update(byComp) 2043 2044 # portfolio distribution by sectors: 2045 view["analytics"]["distrBySectors"]["All money cash"] = { 2046 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2047 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2048 } 2049 view["analytics"]["distrBySectors"].update(bySect) 2050 2051 # portfolio distribution by currencies: 2052 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2053 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2054 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2055 2056 view["analytics"]["distrByCurrencies"].update(byCurr) 2057 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2058 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2059 2060 # portfolio distribution by countries: 2061 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2062 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2063 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2064 2065 view["analytics"]["distrByCountries"].update(byCountry) 2066 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2068 2069 # --- Prepare text statistics overview in human-readable: 2070 if show: 2071 # Whatever the value `details`, header not changes: 2072 info = [ 2073 "# Client's portfolio\n\n", 2074 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2075 "* **Account ID:** [{}]\n".format(self.accountId), 2076 ] 2077 2078 if details in ["full", "positions", "digest"]: 2079 info.extend([ 2080 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2081 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2082 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2083 view["stat"]["totalChangesRUB"], 2084 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2085 view["stat"]["totalChangesPercentRUB"], 2086 ), 2087 ]) 2088 2089 if details in ["full", "positions"]: 2090 info.extend([ 2091 "## Open positions\n\n", 2092 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2093 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2094 "| Ruble | {:>31} | | | | | |\n".format( 2095 "{:.2f} ({:.2f}) rub".format( 2096 view["stat"]["availableRUB"], 2097 view["stat"]["blockedRUB"], 2098 ) 2099 ) 2100 ]) 2101 2102 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2103 return [ 2104 "| | | | | | | |\n", 2105 "| {:<27} | | | | | {:>19} | |\n".format( 2106 noTradeStr if noTradeStr else typeStr, 2107 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2108 ), 2109 ] 2110 2111 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2112 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2113 "{} [{}]".format(data["ticker"], data["figi"]), 2114 "{:.2f} ({:.2f}) {}".format( 2115 data["volume"], 2116 data["blocked"], 2117 data["currency"], 2118 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2119 data["volume"], 2120 data["blocked"], 2121 ), 2122 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2123 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2124 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2126 "{}{:.2f} {} ({}{:.2f}%)".format( 2127 "+" if data["profit"] > 0 else "", 2128 data["profit"], data["baseCurrencyName"], 2129 "+" if data["percentProfit"] > 0 else "", 2130 data["percentProfit"], 2131 ), 2132 ) 2133 2134 # --- Show currencies section: 2135 if view["stat"]["Currencies"]: 2136 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2137 for item in view["stat"]["Currencies"]: 2138 info.append(_InfoStr(item, showCurrencyName=True)) 2139 2140 else: 2141 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2142 2143 # --- Show shares section: 2144 if view["stat"]["Shares"]: 2145 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2146 2147 for item in view["stat"]["Shares"]: 2148 info.append(_InfoStr(item)) 2149 2150 else: 2151 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2152 2153 # --- Show bonds section: 2154 if view["stat"]["Bonds"]: 2155 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2156 2157 for item in view["stat"]["Bonds"]: 2158 info.append(_InfoStr(item)) 2159 2160 else: 2161 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2162 2163 # --- Show etfs section: 2164 if view["stat"]["Etfs"]: 2165 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2166 2167 for item in view["stat"]["Etfs"]: 2168 info.append(_InfoStr(item)) 2169 2170 else: 2171 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2172 2173 # --- Show futures section: 2174 if view["stat"]["Futures"]: 2175 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2176 2177 for item in view["stat"]["Futures"]: 2178 info.append(_InfoStr(item)) 2179 2180 else: 2181 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2182 2183 if details in ["full", "orders"]: 2184 # --- Show pending orders section: 2185 if view["stat"]["orders"]: 2186 info.extend([ 2187 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2188 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2189 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2190 ]) 2191 2192 for item in view["stat"]["orders"]: 2193 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2194 "{} [{}]".format(item["ticker"], item["figi"]), 2195 item["orderID"], 2196 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2197 "{} {} ({}{:.2f}%)".format( 2198 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2199 item["baseCurrencyName"], 2200 "+" if item["percentChanges"] > 0 else "", 2201 float(item["percentChanges"]), 2202 ), 2203 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2204 item["action"], 2205 item["type"], 2206 item["date"], 2207 )) 2208 2209 else: 2210 info.append("\n## Total pending limit-orders: 0\n") 2211 2212 # --- Show stop orders section: 2213 if view["stat"]["stopOrders"]: 2214 info.extend([ 2215 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2216 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2217 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2218 ]) 2219 2220 for item in view["stat"]["stopOrders"]: 2221 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2222 "{} [{}]".format(item["ticker"], item["figi"]), 2223 item["orderID"], 2224 item["lotsRequested"], 2225 "{} {} ({}{:.2f}%)".format( 2226 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2227 item["baseCurrencyName"], 2228 "+" if item["percentChanges"] > 0 else "", 2229 float(item["percentChanges"]), 2230 ), 2231 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2232 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2233 item["action"], 2234 item["type"], 2235 item["expType"], 2236 item["createDate"], 2237 item["expDate"], 2238 )) 2239 2240 else: 2241 info.append("\n## Total stop-orders: 0\n") 2242 2243 if details in ["full", "analytics"]: 2244 # -- Show analytics section: 2245 if view["stat"]["portfolioCostRUB"] > 0: 2246 info.extend([ 2247 "\n# Analytics\n" 2248 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2249 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2250 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2251 view["stat"]["totalChangesRUB"], 2252 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2253 view["stat"]["totalChangesPercentRUB"], 2254 ), 2255 "\n## Portfolio distribution by assets\n" 2256 "\n| Type | Uniques | Percent | Current cost |\n", 2257 "|------------|---------|---------|--------------------|\n", 2258 ]) 2259 2260 for key in view["analytics"]["distrByAssets"].keys(): 2261 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2262 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2263 key, 2264 view["analytics"]["distrByAssets"][key]["uniques"], 2265 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2266 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2267 )) 2268 2269 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2270 info.extend([ 2271 "\n## Portfolio distribution by companies\n" 2272 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2273 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2274 ]) 2275 2276 for company in view["analytics"]["distrByCompanies"].keys(): 2277 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2278 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2279 info.append("| {} | {:<7} | {:<18} |\n".format( 2280 "{}{}{}".format( 2281 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2282 company, 2283 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2284 ), 2285 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2286 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2287 )) 2288 2289 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2290 info.extend([ 2291 "\n## Portfolio distribution by sectors\n" 2292 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2293 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2294 ]) 2295 2296 for sector in view["analytics"]["distrBySectors"].keys(): 2297 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2298 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2299 sector, 2300 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2301 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2302 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2303 )) 2304 2305 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2306 info.extend([ 2307 "\n## Portfolio distribution by currencies\n" 2308 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2309 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2310 ]) 2311 2312 for curr in view["analytics"]["distrByCurrencies"].keys(): 2313 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2314 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2315 info.append("| {} | {:<7} | {:<18} |\n".format( 2316 "[{}] {}{}".format( 2317 curr, 2318 view["analytics"]["distrByCurrencies"][curr]["name"], 2319 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2320 ), 2321 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2322 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2323 )) 2324 2325 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2326 info.extend([ 2327 "\n## Portfolio distribution by countries\n" 2328 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2329 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2330 ]) 2331 2332 for country in view["analytics"]["distrByCountries"].keys(): 2333 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2334 nameLen = len(country) 2335 info.append("| {} | {:<7} | {:<18} |\n".format( 2336 "{}{}".format( 2337 country, 2338 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2339 ), 2340 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2341 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2342 )) 2343 2344 infoText = "".join(info) 2345 2346 uLogger.info(infoText) 2347 2348 if details == "full" and self.overviewFile: 2349 filename = self.overviewFile 2350 2351 elif details == "digest" and self.overviewDigestFile: 2352 filename = self.overviewDigestFile 2353 2354 elif details == "positions" and self.overviewPositionsFile: 2355 filename = self.overviewPositionsFile 2356 2357 elif details == "orders" and self.overviewOrdersFile: 2358 filename = self.overviewOrdersFile 2359 2360 elif details == "analytics" and self.overviewAnalyticsFile: 2361 filename = self.overviewAnalyticsFile 2362 2363 else: 2364 filename = "" 2365 2366 if filename: 2367 with open(filename, "w", encoding="UTF-8") as fH: 2368 fH.write(infoText) 2369 2370 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2371 2372 return view 2373 2374 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2375 """ 2376 Returns history operations between two given dates for current `accountId`. 2377 If `reportFile` string is not empty then also save human-readable report. 2378 Shows some statistical data of closed positions. 2379 2380 :param start: see docstring in `GetDatesAsString()` method 2381 :param end: see docstring in `GetDatesAsString()` method 2382 :param show: if `True` then also prints all records to the console. 2383 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2384 :return: original list of dictionaries with history of deals records from API ("operations" key): 2385 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2386 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2387 """ 2388 if self.accountId is None or not self.accountId: 2389 uLogger.error("Variable `accountId` must be defined for using this method!") 2390 raise Exception("Account ID required") 2391 2392 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2393 2394 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2395 2396 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2397 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2398 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2399 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2400 customStat = {} # custom statistics in additional to responseJSON 2401 2402 # --- output report in human-readable format: 2403 if show or self.reportFile: 2404 splitLine1 = "| | | | | |\n" # Summary section 2405 splitLine2 = "| | | | | | | | |\n" # Operations section 2406 nextDay = "" 2407 2408 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2409 2410 if len(ops) > 0: 2411 customStat = { 2412 "opsCount": 0, # total operations count 2413 "buyCount": 0, # buy operations 2414 "sellCount": 0, # sell operations 2415 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2416 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2417 "payIn": {"rub": 0.}, # Deposit brokerage account 2418 "payOut": {"rub": 0.}, # Withdrawals 2419 "divs": {"rub": 0.}, # Dividends income 2420 "coupons": {"rub": 0.}, # Coupon's income 2421 "brokerCom": {"rub": 0.}, # Service commissions 2422 "serviceCom": {"rub": 0.}, # Service commissions 2423 "marginCom": {"rub": 0.}, # Margin commissions 2424 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2425 } 2426 2427 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2428 for item in ops: 2429 if item["state"] == "OPERATION_STATE_EXECUTED": 2430 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2431 2432 # count buy operations: 2433 if "_BUY" in item["operationType"]: 2434 customStat["buyCount"] += 1 2435 2436 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2437 customStat["buyTotal"][item["payment"]["currency"]] += payment 2438 2439 else: 2440 customStat["buyTotal"][item["payment"]["currency"]] = payment 2441 2442 # count sell operations: 2443 elif "_SELL" in item["operationType"]: 2444 customStat["sellCount"] += 1 2445 2446 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2447 customStat["sellTotal"][item["payment"]["currency"]] += payment 2448 2449 else: 2450 customStat["sellTotal"][item["payment"]["currency"]] = payment 2451 2452 # count incoming operations: 2453 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2454 if item["payment"]["currency"] in customStat["payIn"].keys(): 2455 customStat["payIn"][item["payment"]["currency"]] += payment 2456 2457 else: 2458 customStat["payIn"][item["payment"]["currency"]] = payment 2459 2460 # count withdrawals operations: 2461 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2462 if item["payment"]["currency"] in customStat["payOut"].keys(): 2463 customStat["payOut"][item["payment"]["currency"]] += payment 2464 2465 else: 2466 customStat["payOut"][item["payment"]["currency"]] = payment 2467 2468 # count dividends income: 2469 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2470 if item["payment"]["currency"] in customStat["divs"].keys(): 2471 customStat["divs"][item["payment"]["currency"]] += payment 2472 2473 else: 2474 customStat["divs"][item["payment"]["currency"]] = payment 2475 2476 # count coupon's income: 2477 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2478 if item["payment"]["currency"] in customStat["coupons"].keys(): 2479 customStat["coupons"][item["payment"]["currency"]] += payment 2480 2481 else: 2482 customStat["coupons"][item["payment"]["currency"]] = payment 2483 2484 # count broker commissions: 2485 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2486 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2487 customStat["brokerCom"][item["payment"]["currency"]] += payment 2488 2489 else: 2490 customStat["brokerCom"][item["payment"]["currency"]] = payment 2491 2492 # count service commissions: 2493 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2494 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2495 customStat["serviceCom"][item["payment"]["currency"]] += payment 2496 2497 else: 2498 customStat["serviceCom"][item["payment"]["currency"]] = payment 2499 2500 # count margin commissions: 2501 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2502 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2503 customStat["marginCom"][item["payment"]["currency"]] += payment 2504 2505 else: 2506 customStat["marginCom"][item["payment"]["currency"]] = payment 2507 2508 # count withholding taxes: 2509 elif "_TAX" in item["operationType"]: 2510 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2511 customStat["allTaxes"][item["payment"]["currency"]] += payment 2512 2513 else: 2514 customStat["allTaxes"][item["payment"]["currency"]] = payment 2515 2516 else: 2517 continue 2518 2519 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2520 2521 # --- view "Actions" lines: 2522 info.extend([ 2523 "| 1 | 2 | 3 | 4 | 5 |\n", 2524 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2525 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2526 "| | Buy: {:<22} | {:<28} | | |\n".format( 2527 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2528 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2529 ), 2530 "| | Sell: {:<21} | {:<28} | | |\n".format( 2531 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2532 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2533 ), 2534 ]) 2535 2536 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2537 for key in opsKeys: 2538 if key == "rub": 2539 continue 2540 2541 info.extend([ 2542 "| | | {:<28} | | |\n".format( 2543 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2544 ), 2545 "| | | {:<28} | | |\n".format( 2546 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2547 ), 2548 ]) 2549 2550 info.append(splitLine1) 2551 2552 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2553 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2554 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2555 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2558 ) 2559 2560 # --- view "Payments" lines: 2561 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2562 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2563 2564 for key in paymentsKeys: 2565 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2566 2567 info.append(splitLine1) 2568 2569 # --- view "Commissions and taxes" lines: 2570 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2571 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2572 2573 for key in comKeys: 2574 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2575 2576 info.append(splitLine1) 2577 2578 info.extend([ 2579 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2580 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2581 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2582 ]) 2583 2584 else: 2585 info.append("Broker returned no operations during this period\n") 2586 2587 # --- view "Operations" section: 2588 for item in ops: 2589 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2590 continue 2591 2592 else: 2593 self.figi = item["figi"] if item["figi"] else "" 2594 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2595 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2596 2597 # group of deals during one day: 2598 if nextDay and item["date"].split("T")[0] != nextDay: 2599 info.append(splitLine2) 2600 nextDay = "" 2601 2602 else: 2603 nextDay = item["date"].split("T")[0] # saving current day for splitting 2604 2605 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2606 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2607 self.figi if self.figi else "—", 2608 instrument["ticker"] if instrument else "—", 2609 instrument["type"] if instrument else "—", 2610 item["quantity"] if int(item["quantity"]) > 0 else "—", 2611 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2612 TKS_OPERATION_STATES[item["state"]], 2613 TKS_OPERATION_TYPES[item["operationType"]], 2614 )) 2615 2616 infoText = "".join(info) 2617 2618 if show: 2619 uLogger.info(infoText) 2620 2621 if self.reportFile: 2622 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2623 fH.write(infoText) 2624 2625 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2626 2627 return ops, customStat 2628 2629 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2630 """ 2631 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2632 2633 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2634 Warning! Broker server used ISO UTC time by default. 2635 2636 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2637 Also, `historyFile` used to update history with `onlyMissing` parameter. 2638 2639 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2640 2641 :param start: see docstring in `GetDatesAsString()` method. 2642 :param end: see docstring in `GetDatesAsString()` method. 2643 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2644 `"hour"`, `"day"`. Default: `"hour"`. 2645 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2646 False by default. Warning! History appends only from last candle to current time 2647 with always update last candle! 2648 :param csvSep: separator if csv-file is used, `,` by default. 2649 :param show: if `True` then also prints Pandas DataFrame to the console. 2650 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2651 `["date", "time", "open", "high", "low", "close", "volume"]`. 2652 """ 2653 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2654 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2655 history = None # empty pandas object for history 2656 2657 if interval not in TKS_CANDLE_INTERVALS.keys(): 2658 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2659 raise Exception("Incorrect value") 2660 2661 if not (self.ticker or self.figi): 2662 uLogger.error("Ticker or FIGI must be defined!") 2663 raise Exception("Ticker or FIGI required") 2664 2665 if self.ticker and not self.figi: 2666 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2667 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2668 2669 if self.figi and not self.ticker: 2670 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2671 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2672 2673 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2674 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2675 if interval.lower() != "day": 2676 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2677 2678 delta = dtEnd - dtStart # current UTC time minus last time in file 2679 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2680 2681 # calculate history length in candles: 2682 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2683 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2684 length += 1 # to avoid fraction time 2685 2686 # calculate data blocks count: 2687 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2688 2689 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2690 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2691 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2692 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2693 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2694 2695 tempOld = None # pandas object for old history, if --only-missing key present 2696 lastTime = None # datetime object of last old candle in file 2697 2698 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2699 uLogger.debug("--only-missing key present, add only last missing candles...") 2700 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2701 2702 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2703 2704 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2705 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2706 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2707 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2708 2709 # get last datetime object from last string in file or minus 1 delta if file is empty: 2710 if len(tempOld) > 0: 2711 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2712 2713 else: 2714 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2715 2716 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2717 2718 responseJSONs = [] # raw history blocks of data 2719 2720 blockEnd = dtEnd 2721 for item in range(blocks): 2722 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2723 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2724 2725 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2726 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2727 )) 2728 2729 if blockStart == blockEnd: 2730 uLogger.debug("Skipped this zero-length block...") 2731 2732 else: 2733 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2734 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2735 self.body = str({ 2736 "figi": self.figi, 2737 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2738 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2739 "interval": TKS_CANDLE_INTERVALS[interval][0] 2740 }) 2741 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2742 2743 if "code" in responseJSON.keys(): 2744 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2745 2746 else: 2747 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2748 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2749 2750 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2751 2752 blockEnd = blockStart 2753 2754 printCount = len(responseJSONs) # candles to show in console 2755 if responseJSONs: 2756 tempHistory = pd.DataFrame( 2757 data={ 2758 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2759 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2761 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2762 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2763 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2764 "volume": [int(item["volume"]) for item in responseJSONs], 2765 }, 2766 index=range(len(responseJSONs)), 2767 columns=["date", "time", "open", "high", "low", "close", "volume"], 2768 ) 2769 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2770 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2771 2772 # append only newest candles to old history if --only-missing key present: 2773 if onlyMissing and tempOld is not None and lastTime is not None: 2774 index = 0 # find start index in tempHistory data: 2775 2776 for i, item in tempHistory.iterrows(): 2777 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2778 2779 if curTime == lastTime: 2780 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2781 index = i 2782 printCount = index + 1 2783 break 2784 2785 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2786 2787 else: 2788 history = tempHistory # if no `--only-missing` key then load full data from server 2789 2790 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2791 2792 if history is not None and not history.empty: 2793 if show: 2794 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2795 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2796 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2797 )) 2798 2799 else: 2800 uLogger.warning("Received an empty candles history!") 2801 2802 if self.historyFile is not None: 2803 if history is not None and not history.empty: 2804 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2805 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2806 2807 else: 2808 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2809 2810 else: 2811 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2812 2813 return history 2814 2815 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2816 """ 2817 Load candles history from csv-file and return Pandas DataFrame object. 2818 2819 See also: `History()` and `ShowHistoryChart()` methods. 2820 2821 :param filePath: path to csv-file to open. 2822 """ 2823 loadedHistory = None # init candles data object 2824 2825 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2826 2827 if os.path.exists(filePath): 2828 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2829 2830 tfStr = self.priceModel.FormattedDelta( 2831 self.priceModel.timeframe, 2832 "{days} days {hours}h {minutes}m {seconds}s", 2833 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2834 self.priceModel.timeframe, 2835 "{hours}h {minutes}m {seconds}s", 2836 ) 2837 2838 if loadedHistory is not None and not loadedHistory.empty: 2839 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2840 len(loadedHistory), 2841 tfStr, 2842 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2843 ) 2844 2845 else: 2846 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2847 2848 else: 2849 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2850 2851 return loadedHistory 2852 2853 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2854 """ 2855 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2856 2857 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2858 Default: `index.html` (both for interact and non-interact candlesticks chart). 2859 2860 See also: `History()` and `LoadHistory()` methods. 2861 2862 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2863 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2864 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2865 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2866 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2867 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2868 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2869 """ 2870 if isinstance(candles, str): 2871 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2872 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2873 2874 elif isinstance(candles, pd.DataFrame): 2875 self.priceModel.prices = candles # set candles chain from variable 2876 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2877 2878 if "datetime" not in candles.columns: 2879 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2880 2881 else: 2882 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2883 raise Exception("Incorrect value") 2884 2885 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2886 2887 if interact: 2888 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2889 2890 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2891 2892 else: 2893 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2894 2895 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2896 2897 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2898 2899 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2900 """ 2901 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2902 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2903 2904 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2905 2906 :param operation: string "Buy" or "Sell". 2907 :param lots: volume, integer count of lots >= 1. 2908 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2909 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2910 :param expDate: string "Undefined" by default or local date in future, 2911 it is a string with format `%Y-%m-%d %H:%M:%S`. 2912 :return: JSON with response from broker server. 2913 """ 2914 if self.accountId is None or not self.accountId: 2915 uLogger.error("Variable `accountId` must be defined for using this method!") 2916 raise Exception("Account ID required") 2917 2918 if operation is None or not operation or operation not in ("Buy", "Sell"): 2919 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2920 raise Exception("Incorrect value") 2921 2922 if lots is None or lots < 1: 2923 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2924 lots = 1 2925 2926 if tp is None or tp < 0: 2927 tp = 0 2928 2929 if sl is None or sl < 0: 2930 sl = 0 2931 2932 if expDate is None or not expDate: 2933 expDate = "Undefined" 2934 2935 if not (self.ticker or self.figi): 2936 uLogger.error("Ticker or FIGI must be defined!") 2937 raise Exception("Ticker or FIGI required") 2938 2939 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2940 self.ticker = instrument["ticker"] 2941 self.figi = instrument["figi"] 2942 2943 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2944 2945 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2946 self.body = str({ 2947 "figi": self.figi, 2948 "quantity": str(lots), 2949 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2950 "accountId": str(self.accountId), 2951 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2952 }) 2953 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2954 2955 if "orderId" in response.keys(): 2956 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2957 operation, response["orderId"], 2958 self.ticker, self.figi, lots, 2959 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2960 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2961 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2962 )) 2963 2964 else: 2965 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2966 2967 if tp > 0: 2968 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2969 2970 if sl > 0: 2971 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2972 2973 return response 2974 2975 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2976 """ 2977 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2978 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2979 2980 See also: `Order()` and `Trade()` docstrings. 2981 2982 :param lots: volume, integer count of lots >= 1. 2983 :param tp: float > 0, take profit price of stop-order. 2984 :param sl: float > 0, stop loss price of stop-order. 2985 :param expDate: it's a local date in future. 2986 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2987 :return: JSON with response from broker server. 2988 """ 2989 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2990 2991 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2992 """ 2993 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2994 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2995 2996 See also: `Order()` and `Trade()` docstrings. 2997 2998 :param lots: volume, integer count of lots >= 1. 2999 :param tp: float > 0, take profit price of stop-order. 3000 :param sl: float > 0, stop loss price of stop-order. 3001 :param expDate: it's a local date in the future. 3002 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3003 :return: JSON with response from broker server. 3004 """ 3005 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3006 3007 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 3008 """ 3009 Close position of given instruments. 3010 3011 :param tickers: tickers list of instruments that must be closed. 3012 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3013 This avoids unnecessary downloading data from the server. 3014 """ 3015 if not tickers: 3016 uLogger.info("Tickers list is empty, nothing to close.") 3017 3018 else: 3019 if portfolio is None or not portfolio: 3020 portfolio = self.Overview(show=False) 3021 3022 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3023 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3024 3025 for ticker in tickers: 3026 if ticker not in allOpenedTickers: 3027 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3028 continue 3029 3030 # search open trade info about instrument by ticker: 3031 instrument = {} 3032 for iType in TKS_INSTRUMENTS: 3033 if instrument: 3034 break 3035 3036 for item in portfolio["stat"][iType]: 3037 if item["ticker"] == ticker: 3038 instrument = item 3039 break 3040 3041 if instrument: 3042 self.ticker = ticker 3043 self.figi = instrument["figi"] 3044 3045 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3046 self.ticker, 3047 self.figi, 3048 int(instrument["volume"]), 3049 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3050 )) 3051 3052 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3053 3054 if tradeLots > 0: 3055 if instrument["blocked"] > 0: 3056 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3057 instrument["blocked"], 3058 self.ticker, 3059 tradeLots, 3060 )) 3061 3062 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3063 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3064 3065 else: 3066 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3067 3068 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3069 """ 3070 Close all positions of given instruments with defined type. 3071 3072 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3073 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3074 This avoids unnecessary downloading data from the server. 3075 """ 3076 if iType not in TKS_INSTRUMENTS: 3077 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3078 3079 else: 3080 if portfolio is None or not portfolio: 3081 portfolio = self.Overview(show=False) 3082 3083 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3084 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3085 3086 if tickers and portfolio: 3087 self.CloseTrades(tickers, portfolio) 3088 3089 else: 3090 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3091 3092 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3093 """ 3094 Universal method to create market or limit orders with all available parameters for current `accountId`. 3095 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3096 3097 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3098 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3099 3100 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3101 then broker immediately open market order as you can do simple --buy or --sell operations! 3102 3103 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3104 When current price will go up or down to target price value then broker opens a limit order. 3105 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3106 3107 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3108 3109 :param operation: string "Buy" or "Sell". 3110 :param orderType: string "Limit" or "Stop". 3111 :param lots: volume, integer count of lots >= 1. 3112 :param targetPrice: target price > 0. This is open trade price for limit order. 3113 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3114 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3115 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3116 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3117 Stop loss order always executed by market price. 3118 :param expDate: string "Undefined" by default or local date in future. 3119 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3120 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3121 A limit order has no expiration date, it lasts until the end of the trading day. 3122 :return: JSON with response from broker server. 3123 """ 3124 if self.accountId is None or not self.accountId: 3125 uLogger.error("Variable `accountId` must be defined for using this method!") 3126 raise Exception("Account ID required") 3127 3128 if operation is None or not operation or operation not in ("Buy", "Sell"): 3129 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3130 raise Exception("Incorrect value") 3131 3132 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3133 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3134 raise Exception("Incorrect value") 3135 3136 if lots is None or lots < 1: 3137 uLogger.error("You must define trade volume > 0: integer count of lots!") 3138 raise Exception("Incorrect value") 3139 3140 if targetPrice is None or targetPrice <= 0: 3141 uLogger.error("Target price for limit-order must be greater than 0!") 3142 raise Exception("Incorrect value") 3143 3144 if limitPrice is None or limitPrice <= 0: 3145 limitPrice = targetPrice 3146 3147 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3148 stopType = "Limit" 3149 3150 if expDate is None or not expDate: 3151 expDate = "Undefined" 3152 3153 if not (self.ticker or self.figi): 3154 uLogger.error("Tocker or FIGI must be defined!") 3155 raise Exception("Ticker or FIGI required") 3156 3157 response = {} 3158 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3159 self.ticker = instrument["ticker"] 3160 self.figi = instrument["figi"] 3161 3162 if orderType == "Limit": 3163 uLogger.debug( 3164 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3165 self.ticker, self.figi, 3166 operation, lots, targetPrice, instrument["currency"], 3167 )) 3168 3169 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3170 self.body = str({ 3171 "figi": self.figi, 3172 "quantity": str(lots), 3173 "price": FloatToNano(targetPrice), 3174 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3175 "accountId": str(self.accountId), 3176 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3177 }) 3178 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3179 3180 if "orderId" in response.keys(): 3181 uLogger.info( 3182 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3183 response["orderId"], 3184 self.ticker, self.figi, 3185 operation, lots, targetPrice, instrument["currency"], 3186 )) 3187 3188 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3189 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3190 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3191 targetPrice, instrument["currency"], 3192 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3193 )) 3194 3195 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3196 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3197 targetPrice, instrument["currency"], 3198 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3199 )) 3200 3201 else: 3202 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3203 3204 if orderType == "Stop": 3205 uLogger.debug( 3206 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3207 self.ticker, self.figi, 3208 operation, lots, 3209 targetPrice, instrument["currency"], 3210 limitPrice, instrument["currency"], 3211 stopType, expDate, 3212 )) 3213 3214 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3215 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3216 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3217 3218 body = { 3219 "figi": self.figi, 3220 "quantity": str(lots), 3221 "price": FloatToNano(limitPrice), 3222 "stopPrice": FloatToNano(targetPrice), 3223 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3224 "accountId": str(self.accountId), 3225 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3226 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3227 } 3228 3229 if expDateUTC: 3230 body["expireDate"] = expDateUTC 3231 3232 self.body = str(body) 3233 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3234 3235 if "stopOrderId" in response.keys(): 3236 uLogger.info( 3237 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3238 response["stopOrderId"], 3239 self.ticker, self.figi, 3240 operation, lots, 3241 targetPrice, instrument["currency"], 3242 limitPrice, instrument["currency"], 3243 TKS_STOP_ORDER_TYPES[stopOrderType], 3244 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3245 )) 3246 3247 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3248 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3249 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3250 targetPrice, instrument["currency"], 3251 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3252 )) 3253 3254 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3255 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3256 targetPrice, instrument["currency"], 3257 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3258 )) 3259 3260 else: 3261 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3262 3263 return response 3264 3265 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3266 """ 3267 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3268 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3269 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3270 See also: `Order()` docstring. 3271 3272 :param lots: volume, integer count of lots >= 1. 3273 :param targetPrice: target price > 0. This is open trade price for limit order. 3274 :return: JSON with response from broker server. 3275 """ 3276 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3277 3278 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3279 """ 3280 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3281 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3282 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3283 target price value then broker opens a limit order. See also: `Order()` docstring. 3284 3285 :param lots: volume, integer count of lots >= 1. 3286 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3287 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3288 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3289 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3290 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3291 :param expDate: string "Undefined" by default or local date in future. 3292 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3293 This date is converting to UTC format for server. 3294 :return: JSON with response from broker server. 3295 """ 3296 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3297 3298 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3299 """ 3300 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3301 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3302 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3303 See also: `Order()` docstring. 3304 3305 :param lots: volume, integer count of lots >= 1. 3306 :param targetPrice: target price > 0. This is open trade price for limit order. 3307 :return: JSON with response from broker server. 3308 """ 3309 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3310 3311 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3312 """ 3313 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3314 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3315 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3316 target price value then broker opens a limit order. See also: `Order()` docstring. 3317 3318 :param lots: volume, integer count of lots >= 1. 3319 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3320 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3321 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3322 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3323 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3324 :param expDate: string "Undefined" by default or local date in future. 3325 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3326 This date is converting to UTC format for server. 3327 :return: JSON with response from broker server. 3328 """ 3329 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3330 3331 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3332 """ 3333 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3334 3335 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3336 :param allOrdersIDs: pre-received lists of all active pending orders. 3337 This avoids unnecessary downloading data from the server. 3338 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3339 """ 3340 if self.accountId is None or not self.accountId: 3341 uLogger.error("Variable `accountId` must be defined for using this method!") 3342 raise Exception("Account ID required") 3343 3344 if orderIDs: 3345 if allOrdersIDs is None or not allOrdersIDs: 3346 rawOrders = self.RequestPendingOrders() 3347 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3348 3349 if allStopOrdersIDs is None or not allStopOrdersIDs: 3350 rawStopOrders = self.RequestStopOrders() 3351 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3352 3353 for orderID in orderIDs: 3354 idInPendingOrders = orderID in allOrdersIDs 3355 idInStopOrders = orderID in allStopOrdersIDs 3356 3357 if not (idInPendingOrders or idInStopOrders): 3358 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3359 continue 3360 3361 else: 3362 if idInPendingOrders: 3363 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3364 3365 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3366 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3367 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3368 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3369 3370 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3371 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3372 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3373 3374 else: 3375 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3376 3377 elif idInStopOrders: 3378 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3379 3380 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3381 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3382 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3383 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3384 3385 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3386 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3387 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3388 3389 else: 3390 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3391 3392 else: 3393 continue 3394 3395 def CloseAllOrders(self) -> None: 3396 """ 3397 Gets a list of open pending and stop orders and cancel it all. 3398 """ 3399 rawOrders = self.RequestPendingOrders() 3400 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3401 lenOrders = len(allOrdersIDs) 3402 3403 rawStopOrders = self.RequestStopOrders() 3404 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3405 lenSOrders = len(allStopOrdersIDs) 3406 3407 if lenOrders > 0 or lenSOrders > 0: 3408 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3409 3410 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3411 3412 else: 3413 uLogger.info("Orders not found, nothing to cancel.") 3414 3415 def CloseAll(self, *args) -> None: 3416 """ 3417 Close all available (not blocked) opened trades and orders. 3418 3419 Also, you can select one or more keywords case-insensitive: 3420 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3421 3422 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3423 """ 3424 overview = self.Overview(show=False) # get all open trades info 3425 3426 if len(args) == 0: 3427 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3428 self.CloseAllOrders() # close all pending and stop orders 3429 3430 for iType in TKS_INSTRUMENTS: 3431 if iType != "Currencies": 3432 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3433 3434 else: 3435 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3436 lowerArgs = [x.lower() for x in args] 3437 3438 if "orders" in lowerArgs: 3439 self.CloseAllOrders() # close all pending and stop orders 3440 3441 for iType in TKS_INSTRUMENTS: 3442 if iType.lower() in lowerArgs and iType != "Currencies": 3443 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3444 3445 @staticmethod 3446 def ParseOrderParameters(operation, **inputParameters): 3447 """ 3448 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3449 3450 :param operation: string "Buy" or "Sell". 3451 :param inputParameters: this is dict of strings that looks like this 3452 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3453 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3454 "prices" key: one or more prices to open limit-orders 3455 Counts of values in lots and prices lists must be equals! 3456 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3457 """ 3458 # TODO: update order grid work with api v2 3459 pass 3460 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3461 # 3462 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3463 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3464 # raise Exception("Incorrect value") 3465 # 3466 # if "l" in inputParameters.keys(): 3467 # inputParameters["lots"] = inputParameters.pop("l") 3468 # 3469 # if "p" in inputParameters.keys(): 3470 # inputParameters["prices"] = inputParameters.pop("p") 3471 # 3472 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3473 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3474 # raise Exception("Incorrect value") 3475 # 3476 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3477 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3478 # 3479 # if len(lots) != len(prices): 3480 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3481 # raise Exception("Incorrect value") 3482 # 3483 # uLogger.debug("Extracted parameters for orders:") 3484 # uLogger.debug("lots = {}".format(lots)) 3485 # uLogger.debug("prices = {}".format(prices)) 3486 # 3487 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3488 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3489 # uLogger.debug("Order parameters: {}".format(result)) 3490 # 3491 # return result 3492 3493 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3494 """ 3495 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3496 3497 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3498 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3499 """ 3500 result = False 3501 msg = "Instrument not defined!" 3502 3503 if portfolio is None or not portfolio: 3504 portfolio = self.Overview(show=False) 3505 3506 if self.ticker: 3507 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3508 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3509 3510 for iType in TKS_INSTRUMENTS: 3511 for instrument in portfolio["stat"][iType]: 3512 if instrument["ticker"] == self.ticker: 3513 result = True 3514 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3515 break 3516 3517 elif self.figi: 3518 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3519 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3520 3521 for iType in TKS_INSTRUMENTS: 3522 for instrument in portfolio["stat"][iType]: 3523 if instrument["figi"] == self.figi: 3524 result = True 3525 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3526 break 3527 3528 else: 3529 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3530 3531 uLogger.debug(msg) 3532 3533 return result 3534 3535 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3536 """ 3537 Returns instrument is in the user's portfolio if it presents there. 3538 Instrument must be defined by `ticker` (highly priority) or `figi`. 3539 3540 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3541 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3542 """ 3543 result = None 3544 msg = "Instrument not defined!" 3545 3546 if portfolio is None or not portfolio: 3547 portfolio = self.Overview(show=False) 3548 3549 if self.ticker: 3550 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3551 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3552 3553 for iType in TKS_INSTRUMENTS: 3554 for instrument in portfolio["stat"][iType]: 3555 if instrument["ticker"] == self.ticker: 3556 result = instrument 3557 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3558 break 3559 3560 elif self.figi: 3561 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3562 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3563 3564 for iType in TKS_INSTRUMENTS: 3565 for instrument in portfolio["stat"][iType]: 3566 if instrument["figi"] == self.figi: 3567 result = instrument 3568 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3569 break 3570 3571 else: 3572 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3573 3574 uLogger.debug(msg) 3575 3576 return result 3577 3578 def RequestLimits(self) -> dict: 3579 """ 3580 Method for obtaining the available funds for withdrawal for current `accountId`. 3581 3582 See also: 3583 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3584 - `OverviewLimits()` method 3585 3586 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3587 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3588 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3589 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3590 """ 3591 if self.accountId is None or not self.accountId: 3592 uLogger.error("Variable `accountId` must be defined for using this method!") 3593 raise Exception("Account ID required") 3594 3595 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3596 3597 self.body = str({"accountId": self.accountId}) 3598 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3599 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3600 3601 uLogger.debug("Records about available funds for withdrawal successfully received") 3602 3603 return rawLimits 3604 3605 def OverviewLimits(self, show: bool = False) -> dict: 3606 """ 3607 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3608 3609 See also: `RequestLimits()`. 3610 3611 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3612 :return: dict with raw parsed data from server and some calculated statistics about it. 3613 """ 3614 if self.accountId is None or not self.accountId: 3615 uLogger.error("Variable `accountId` must be defined for using this method!") 3616 raise Exception("Account ID required") 3617 3618 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3619 3620 view = { 3621 "rawLimits": rawLimits, 3622 "limits": { # parsed data for every currency: 3623 "money": { # this is an array of portfolio currency positions 3624 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3625 }, 3626 "blocked": { # this is an array of blocked currency 3627 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3628 }, 3629 "blockedGuarantee": { # this is locked money under collateral for futures 3630 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3631 }, 3632 }, 3633 } 3634 3635 # --- Prepare text table with limits in human-readable format: 3636 if show: 3637 info = [ 3638 "# Withdrawal limits\n\n", 3639 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3640 "* **Account ID:** [{}]\n".format(self.accountId), 3641 ] 3642 3643 if view["limits"]["money"]: 3644 info.extend([ 3645 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3646 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3647 ]) 3648 3649 else: 3650 info.append("\nNo withdrawal limits\n") 3651 3652 for curr in view["limits"]["money"].keys(): 3653 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3654 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3655 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3656 3657 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3658 "[{}]".format(curr), 3659 "{:.2f}".format(view["limits"]["money"][curr]), 3660 "{:.2f}".format(availableMoney), 3661 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3662 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3663 ) 3664 3665 if curr == "rub": 3666 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3667 3668 else: 3669 info.append(infoStr) 3670 3671 infoText = "".join(info) 3672 3673 uLogger.info(infoText) 3674 3675 if self.withdrawalLimitsFile: 3676 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3677 fH.write(infoText) 3678 3679 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3680 3681 return view 3682 3683 def RequestAccounts(self) -> dict: 3684 """ 3685 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3686 3687 See also: 3688 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3689 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3690 - `OverviewUserInfo()` method 3691 3692 :return: dict with raw data from server that contains accounts info. Example of dict: 3693 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3694 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3695 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3696 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3697 """ 3698 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3699 3700 self.body = str({}) 3701 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3702 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3703 3704 uLogger.debug("Records about available accounts successfully received") 3705 3706 return rawAccounts 3707 3708 def RequestUserInfo(self) -> dict: 3709 """ 3710 Method for requesting common user's information. 3711 3712 See also: 3713 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3714 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3715 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3716 - `OverviewUserInfo()` method 3717 3718 :return: dict with raw data from server that contains user's information. Example of dict: 3719 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3720 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3721 """ 3722 uLogger.debug("Requesting common user's information. Wait, please...") 3723 3724 self.body = str({}) 3725 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3726 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3727 3728 uLogger.debug("Records about current user successfully received") 3729 3730 return rawUserInfo 3731 3732 def RequestMarginStatus(self, accountId: str = None) -> dict: 3733 """ 3734 Method for requesting margin calculation for defined account ID. 3735 3736 See also: 3737 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3738 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3739 - `OverviewUserInfo()` method 3740 3741 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3742 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3743 Example of responses: 3744 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3745 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3746 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3747 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3748 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3749 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3750 """ 3751 if accountId is None or not accountId: 3752 if self.accountId is None or not self.accountId: 3753 uLogger.error("Variable `accountId` must be defined for using this method!") 3754 raise Exception("Account ID required") 3755 3756 else: 3757 accountId = self.accountId # use `self.accountId` (main ID) by default 3758 3759 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3760 3761 self.body = str({"accountId": accountId}) 3762 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3763 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3764 3765 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3766 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3767 rawMargin = {} 3768 3769 else: 3770 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3771 3772 return rawMargin 3773 3774 def RequestTariffLimits(self) -> dict: 3775 """ 3776 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3777 3778 See also: 3779 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3780 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3781 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3782 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3783 - `OverviewUserInfo()` method 3784 3785 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3786 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3787 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3788 """ 3789 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3790 3791 self.body = str({}) 3792 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3793 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3794 3795 uLogger.debug("Records with limits of current tariff successfully received") 3796 3797 return rawTariffLimits 3798 3799 def RequestBondCoupons(self, iJSON: dict) -> dict: 3800 """ 3801 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3802 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3803 All dates are in UTC timezone. 3804 3805 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3806 Documentation: 3807 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3808 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3809 3810 See also: `ExtendBondsData()`. 3811 3812 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3813 If raw iJSON is not data of bond then server returns an error [400] with message: 3814 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3815 :return: dictionary with bond payment calendar. Response example 3816 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3817 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3818 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3819 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3820 """ 3821 if iJSON["figi"] is None or not iJSON["figi"]: 3822 uLogger.error("FIGI must be defined for using this method!") 3823 raise Exception("FIGI required") 3824 3825 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3826 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3827 3828 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3829 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3830 self.figi, 3831 startDate, 3832 endDate, 3833 )) 3834 3835 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3836 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3837 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3838 3839 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3840 uLogger.warning("Instrument type is not bond!") 3841 3842 else: 3843 uLogger.debug("Records about bond payment calendar successfully received") 3844 3845 return calendar 3846 3847 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3848 """ 3849 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3850 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3851 coupon yields, current yields and some statistics etc. 3852 3853 WARNING! This is too long operation if a lot of bonds requested from broker server. 3854 3855 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3856 3857 :param instruments: list of strings with tickers or FIGIs. 3858 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3859 for further used by data scientists or stock analytics. 3860 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3861 In XLSX-file and Pandas DataFrame fields mean: 3862 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3863 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3864 """ 3865 if instruments is None or not instruments: 3866 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3867 raise Exception("Ticker or FIGI required") 3868 3869 if isinstance(instruments, str): 3870 instruments = [instruments] 3871 3872 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3873 3874 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3875 3876 iCount = len(uniqueInstruments) 3877 tooLong = iCount >= 20 3878 if tooLong: 3879 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3880 3881 bonds = None 3882 for i, self.figi in enumerate(uniqueInstruments): 3883 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3884 3885 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3886 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3887 rawBond = self.SearchByFIGI(requestPrice=True) 3888 3889 # Widen raw data with UTC current time (iData["actualDateTime"]): 3890 actualDate = datetime.now(tzutc()) 3891 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3892 3893 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3894 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3895 3896 # Replace some values with human-readable: 3897 iData["nominalCurrency"] = iData["nominal"]["currency"] 3898 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3899 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3900 iData["aciCurrency"] = iData["aciValue"]["currency"] 3901 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3902 iData["issueSize"] = int(iData["issueSize"]) 3903 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3904 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3905 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3906 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3907 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3908 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3909 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3910 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3911 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3912 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3913 3914 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3915 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3916 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3917 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3918 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3919 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3920 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3921 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3922 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3923 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3924 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3925 3926 # Widen raw data with calendar data from `rawCalendar` values: 3927 calendarData = [] 3928 for item in iData["rawCalendar"]["events"]: 3929 calendarData.append({ 3930 "couponDate": item["couponDate"], 3931 "couponNumber": int(item["couponNumber"]), 3932 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3933 "payCurrency": item["payOneBond"]["currency"], 3934 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3935 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3936 "couponStartDate": item["couponStartDate"], 3937 "couponEndDate": item["couponEndDate"], 3938 "couponPeriod": item["couponPeriod"], 3939 }) 3940 3941 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3942 if "maturityDate" not in iData.keys(): 3943 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3944 3945 # Widen raw data with Coupon Rate. 3946 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3947 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3948 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3949 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3950 3951 # Widen raw data with Yield to Maturity (YTM) on current date. 3952 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3953 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3954 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3955 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3956 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3957 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3958 3959 iData["calendar"] = calendarData # adds calendar at the end 3960 3961 # Remove not used data: 3962 iData.pop("uid") 3963 iData.pop("positionUid") 3964 iData.pop("currentPrice") 3965 iData.pop("rawCalendar") 3966 3967 colNames = list(iData.keys()) 3968 if bonds is None: 3969 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3970 3971 else: 3972 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3973 3974 else: 3975 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3976 3977 processed = round(100 * (i + 1) / iCount, 1) 3978 if tooLong and processed % 5 == 0: 3979 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3980 3981 else: 3982 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3983 3984 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3985 3986 # Saving bonds from Pandas DataFrame to XLSX sheet: 3987 if xlsx and self.bondsXLSXFile: 3988 with pd.ExcelWriter( 3989 path=self.bondsXLSXFile, 3990 date_format=TKS_DATE_FORMAT, 3991 datetime_format=TKS_DATE_TIME_FORMAT, 3992 mode="w", 3993 ) as writer: 3994 bonds.to_excel( 3995 writer, 3996 sheet_name="Extended bonds data", 3997 index=True, 3998 encoding="UTF-8", 3999 freeze_panes=(1, 1), 4000 ) # saving as XLSX-file with freeze first row and column as headers 4001 4002 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4003 4004 return bonds 4005 4006 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4007 """ 4008 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4009 4010 WARNING! This is too long operation if a lot of bonds requested from broker server. 4011 4012 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4013 4014 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4015 extended information about bonds: main info, current prices, bond payment calendar, 4016 coupon yields, current yields and some statistics etc. 4017 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4018 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4019 for further used by data scientists or stock analytics. 4020 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4021 """ 4022 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4023 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4024 4025 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4026 4027 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4028 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4029 calendar = None 4030 for bond in extBonds.iterrows(): 4031 for item in bond[1]["calendar"]: 4032 cData = { 4033 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4034 "couponDate": item["couponDate"], 4035 "figi": bond[1]["figi"], 4036 "ticker": bond[1]["ticker"], 4037 "name": bond[1]["name"], 4038 "couponNumber": item["couponNumber"], 4039 "payOneBond": item["payOneBond"], 4040 "payCurrency": item["payCurrency"], 4041 "couponType": item["couponType"], 4042 "couponPeriod": item["couponPeriod"], 4043 "fixDate": item["fixDate"], 4044 "couponStartDate": item["couponStartDate"], 4045 "couponEndDate": item["couponEndDate"], 4046 } 4047 4048 if calendar is None: 4049 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4050 4051 else: 4052 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4053 4054 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4055 4056 # Saving calendar from Pandas DataFrame to XLSX sheet: 4057 if xlsx: 4058 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4059 4060 with pd.ExcelWriter( 4061 path=xlsxCalendarFile, 4062 date_format=TKS_DATE_FORMAT, 4063 datetime_format=TKS_DATE_TIME_FORMAT, 4064 mode="w", 4065 ) as writer: 4066 humanReadable = calendar.copy(deep=True) 4067 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4068 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4069 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4070 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4071 humanReadable.columns = colNames # human-readable column names 4072 4073 humanReadable.to_excel( 4074 writer, 4075 sheet_name="Bond payments calendar", 4076 index=False, 4077 encoding="UTF-8", 4078 freeze_panes=(1, 2), 4079 ) # saving as XLSX-file with freeze first row and column as headers 4080 4081 del humanReadable # release df in memory 4082 4083 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4084 4085 return calendar 4086 4087 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4088 """ 4089 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4090 Also, creates Markdown file with calendar data, `calendar.md` by default. 4091 4092 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4093 4094 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4095 extended information about bonds: main info, current prices, bond payment calendar, 4096 coupon yields, current yields and some statistics etc. 4097 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4098 :param show: if `True` then also printing bonds payment calendar to the console, 4099 otherwise save to file `calendarFile` only. `False` by default. 4100 :return: multilines text in Markdown format with bonds payment calendar as a table. 4101 """ 4102 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4103 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4104 4105 infoText = "# Bond payments calendar\n\n" 4106 4107 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4108 4109 if not calendar.empty: 4110 splitLine = "| | | | | | | | | |\n" 4111 4112 info = [ 4113 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4114 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4115 ] 4116 4117 newMonth = False 4118 notOneBond = calendar["figi"].nunique() > 1 4119 for i, bond in enumerate(calendar.iterrows()): 4120 if newMonth and notOneBond: 4121 info.append(splitLine) 4122 4123 info.append( 4124 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4125 " √" if bond[1]["paid"] else " —", 4126 bond[1]["couponDate"].split("T")[0], 4127 bond[1]["figi"], 4128 bond[1]["ticker"], 4129 bond[1]["couponNumber"], 4130 "{} {}".format( 4131 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4132 bond[1]["payCurrency"], 4133 ), 4134 bond[1]["couponType"], 4135 bond[1]["couponPeriod"], 4136 bond[1]["fixDate"].split("T")[0], 4137 ) 4138 ) 4139 4140 if i < len(calendar.values) - 1: 4141 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4142 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4143 newMonth = False if curDate.month == nextDate.month else True 4144 4145 else: 4146 newMonth = False 4147 4148 infoText += "".join(info) 4149 4150 if show: 4151 uLogger.info("{}".format(infoText)) 4152 4153 if self.calendarFile is not None: 4154 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4155 fH.write(infoText) 4156 4157 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4158 4159 else: 4160 infoText += "No data\n" 4161 4162 return infoText 4163 4164 def OverviewAccounts(self, show: bool = False) -> dict: 4165 """ 4166 Method for parsing and show simple table with all available user accounts. 4167 4168 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4169 4170 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4171 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4172 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4173 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4174 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4175 "closed": "—", "access": "Full access" }, ...}}` 4176 """ 4177 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4178 4179 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4180 accounts = { 4181 item["id"]: { 4182 "type": TKS_ACCOUNT_TYPES[item["type"]], 4183 "name": item["name"], 4184 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4185 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4186 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4187 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4188 } for item in rawAccounts["accounts"] 4189 } 4190 4191 # Raw and parsed data with some fields replaced in "stat" section: 4192 view = { 4193 "rawAccounts": rawAccounts, 4194 "stat": accounts, 4195 } 4196 4197 # --- Prepare simple text table with only accounts data in human-readable format: 4198 if show: 4199 info = [ 4200 "# User accounts\n\n", 4201 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4202 "| Account ID | Type | Status | Name |\n", 4203 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4204 ] 4205 4206 for account in view["stat"].keys(): 4207 info.extend([ 4208 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4209 account, 4210 view["stat"][account]["type"], 4211 view["stat"][account]["status"], 4212 view["stat"][account]["name"], 4213 ) 4214 ]) 4215 4216 infoText = "".join(info) 4217 4218 uLogger.info(infoText) 4219 4220 if self.userAccountsFile: 4221 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4222 fH.write(infoText) 4223 4224 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4225 4226 return view 4227 4228 def OverviewUserInfo(self, show: bool = False) -> dict: 4229 """ 4230 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4231 4232 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4233 4234 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4235 :return: dict with raw parsed data from server and some calculated statistics about it. 4236 """ 4237 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4238 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4239 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4240 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4241 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4242 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4243 4244 # This is dict with parsed common user data: 4245 userInfo = { 4246 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4247 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4248 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4249 "tariff": rawUserInfo["tariff"], 4250 } 4251 4252 # This is an array of dict with parsed margin statuses for every account IDs: 4253 margins = {} 4254 for accountId in accounts.keys(): 4255 if rawMargins[accountId]: 4256 margins[accountId] = { 4257 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4258 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4259 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4260 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4261 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4262 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4263 } 4264 4265 else: 4266 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4267 4268 unary = {} # unary-connection limits 4269 for item in rawTariffLimits["unaryLimits"]: 4270 if item["limitPerMinute"] in unary.keys(): 4271 unary[item["limitPerMinute"]].extend(item["methods"]) 4272 4273 else: 4274 unary[item["limitPerMinute"]] = item["methods"] 4275 4276 stream = {} # stream-connection limits 4277 for item in rawTariffLimits["streamLimits"]: 4278 if item["limit"] in stream.keys(): 4279 stream[item["limit"]].extend(item["streams"]) 4280 4281 else: 4282 stream[item["limit"]] = item["streams"] 4283 4284 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4285 limits = { 4286 "unary": unary, 4287 "stream": stream, 4288 } 4289 4290 # Raw and parsed data as an output result: 4291 view = { 4292 "rawUserInfo": rawUserInfo, 4293 "rawAccounts": rawAccounts, 4294 "rawMargins": rawMargins, 4295 "rawTariffLimits": rawTariffLimits, 4296 "stat": { 4297 "userInfo": userInfo, 4298 "accounts": accounts, 4299 "margins": margins, 4300 "limits": limits, 4301 }, 4302 } 4303 4304 # --- Prepare text table with user information in human-readable format: 4305 if show: 4306 info = [ 4307 "# Full user information\n\n", 4308 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4309 "## Common information\n\n", 4310 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4311 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4312 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4313 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4314 "\n## User accounts\n\n", 4315 ] 4316 4317 for account in view["stat"]["accounts"].keys(): 4318 info.extend([ 4319 "### ID: [{}]\n\n".format(account), 4320 "| Parameters | Values |\n", 4321 "|----------------------|--------------------------------------------------------------|\n", 4322 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4323 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4324 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4325 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4326 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4327 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4328 ]) 4329 4330 if margins[account]: 4331 info.extend([ 4332 "| Margin status: | Enabled |\n", 4333 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4334 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4335 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4336 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4337 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4338 ]) 4339 4340 else: 4341 info.append("| Margin status: | Disabled |\n\n") 4342 4343 info.extend([ 4344 "\n## Current user tariff limits\n", 4345 "\nSee also:\n", 4346 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4347 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4348 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4349 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4350 "\n### Unary limits\n", 4351 ]) 4352 4353 if unary: 4354 for key, values in sorted(unary.items()): 4355 info.append("\n* Max requests per minute: {}\n".format(key)) 4356 4357 for value in values: 4358 info.append(" - {}\n".format(value)) 4359 4360 else: 4361 info.append("\nNot available\n") 4362 4363 info.append("\n### Stream limits\n") 4364 4365 if stream: 4366 for key, values in sorted(stream.items()): 4367 info.append("\n* Max stream connections: {}\n".format(key)) 4368 4369 for value in values: 4370 info.append(" - {}\n".format(value)) 4371 4372 else: 4373 info.append("\nNot available\n") 4374 4375 infoText = "".join(info) 4376 4377 uLogger.info(infoText) 4378 4379 if self.userInfoFile: 4380 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4381 fH.write(infoText) 4382 4383 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4384 4385 return view 4386 4387 4388class Args: 4389 """ 4390 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4391 """ 4392 def __init__(self, **kwargs): 4393 self.__dict__.update(kwargs) 4394 4395 def __getattr__(self, item): 4396 return None 4397 4398 4399def ParseArgs(): 4400 """This function get and parse command line keys.""" 4401 parser = ArgumentParser() # command-line string parser 4402 4403 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4404 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4405 4406 # --- options: 4407 4408 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4409 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4410 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4411 4412 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4413 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4414 4415 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4416 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4417 4418 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4419 4420 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4421 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4422 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4423 4424 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4425 4426 # --- commands: 4427 4428 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4429 4430 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4431 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4432 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4433 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4434 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4435 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4436 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4437 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4438 4439 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4440 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4441 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4442 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4443 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4444 4445 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4446 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4447 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4448 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4449 4450 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4451 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4452 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4453 4454 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4455 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4456 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4457 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4458 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4459 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4460 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4461 4462 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4463 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4464 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4465 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4466 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4467 4468 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4469 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4470 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4471 4472 cmdArgs = parser.parse_args() 4473 return cmdArgs 4474 4475 4476def Main(**kwargs): 4477 """ 4478 Main function for work with TKSBrokerAPI in the console. 4479 4480 See examples: 4481 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4482 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4483 """ 4484 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4485 4486 if args.debug_level: 4487 uLogger.level = 10 # always debug level by default 4488 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4489 4490 exitCode = 0 4491 start = datetime.now(tzutc()) 4492 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4493 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4494 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4495 )) 4496 4497 # trying to calculate full current version: 4498 buildVersion = __version__ 4499 try: 4500 v = version("tksbrokerapi") 4501 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4502 4503 except Exception: 4504 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4505 4506 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4507 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4508 4509 try: 4510 if args.version: 4511 print("TKSBrokerAPI {}".format(buildVersion)) 4512 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4513 4514 else: 4515 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4516 server = TinkoffBrokerServer( 4517 token=args.token, 4518 accountId=args.account_id, 4519 useCache=not args.no_cache, 4520 ) 4521 4522 # --- set some options: 4523 4524 if args.ticker: 4525 if args.ticker in server.aliasesKeys: 4526 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4527 4528 else: 4529 server.ticker = args.ticker 4530 4531 if args.figi: 4532 server.figi = args.figi 4533 4534 if args.depth is not None: 4535 server.depth = args.depth 4536 4537 # --- do one of commands: 4538 4539 if args.list: 4540 if args.output is not None: 4541 server.instrumentsFile = args.output 4542 4543 server.ShowInstrumentsInfo(show=True) 4544 4545 elif args.list_xlsx: 4546 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4547 4548 elif args.bonds_xlsx is not None: 4549 if args.output is not None: 4550 server.bondsXLSXFile = args.output 4551 4552 if len(args.bonds_xlsx) == 0: 4553 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4554 4555 else: 4556 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4557 4558 elif args.search: 4559 if args.output is not None: 4560 server.searchResultsFile = args.output 4561 4562 server.SearchInstruments(pattern=args.search[0], show=True) 4563 4564 elif args.info: 4565 if not (args.ticker or args.figi): 4566 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4567 raise Exception("Ticker or FIGI required") 4568 4569 if args.output is not None: 4570 server.infoFile = args.output 4571 4572 if args.ticker: 4573 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4574 4575 else: 4576 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4577 4578 elif args.calendar is not None: 4579 if args.output is not None: 4580 server.calendarFile = args.output 4581 4582 if len(args.calendar) == 0: 4583 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4584 4585 else: 4586 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4587 4588 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4589 4590 elif args.price: 4591 if not (args.ticker or args.figi): 4592 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4593 raise Exception("Ticker or FIGI required") 4594 4595 server.GetCurrentPrices(show=True) 4596 4597 elif args.prices is not None: 4598 if args.output is not None: 4599 server.pricesFile = args.output 4600 4601 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4602 4603 elif args.overview: 4604 if args.output is not None: 4605 server.overviewFile = args.output 4606 4607 server.Overview(show=True, details="full") 4608 4609 elif args.overview_digest: 4610 if args.output is not None: 4611 server.overviewDigestFile = args.output 4612 4613 server.Overview(show=True, details="digest") 4614 4615 elif args.overview_positions: 4616 if args.output is not None: 4617 server.overviewPositionsFile = args.output 4618 4619 server.Overview(show=True, details="positions") 4620 4621 elif args.overview_orders: 4622 if args.output is not None: 4623 server.overviewOrdersFile = args.output 4624 4625 server.Overview(show=True, details="orders") 4626 4627 elif args.overview_analytics: 4628 if args.output is not None: 4629 server.overviewAnalyticsFile = args.output 4630 4631 server.Overview(show=True, details="analytics") 4632 4633 elif args.deals is not None: 4634 if args.output is not None: 4635 server.reportFile = args.output 4636 4637 if 0 <= len(args.deals) < 3: 4638 server.Deals( 4639 start=args.deals[0] if len(args.deals) >= 1 else None, 4640 end=args.deals[1] if len(args.deals) == 2 else None, 4641 show=True, # Always show deals report in console 4642 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4643 ) 4644 4645 else: 4646 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4647 raise Exception("Incorrect value") 4648 4649 elif args.history is not None: 4650 if args.output is not None: 4651 server.historyFile = args.output 4652 4653 if 0 <= len(args.history) < 3: 4654 dataReceived = server.History( 4655 start=args.history[0] if len(args.history) >= 1 else None, 4656 end=args.history[1] if len(args.history) == 2 else None, 4657 interval="hour" if args.interval is None or not args.interval else args.interval, 4658 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4659 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4660 show=True, # shows all downloaded candles in console 4661 ) 4662 4663 if args.render_chart is not None and dataReceived is not None: 4664 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4665 4666 server.ShowHistoryChart( 4667 candles=dataReceived, 4668 interact=iChart, 4669 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4670 ) 4671 4672 else: 4673 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4674 raise Exception("Incorrect value") 4675 4676 elif args.load_history is not None: 4677 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4678 4679 if args.render_chart is not None and histData is not None: 4680 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4681 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4682 4683 server.ShowHistoryChart( 4684 candles=histData, 4685 interact=iChart, 4686 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4687 ) 4688 4689 elif args.trade is not None: 4690 if 1 <= len(args.trade) <= 5: 4691 server.Trade( 4692 operation=args.trade[0], 4693 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4694 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4695 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4696 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4697 ) 4698 4699 else: 4700 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4701 4702 elif args.buy is not None: 4703 if 0 <= len(args.buy) <= 4: 4704 server.Buy( 4705 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4706 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4707 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4708 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4709 ) 4710 4711 else: 4712 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4713 4714 elif args.sell is not None: 4715 if 0 <= len(args.sell) <= 4: 4716 server.Sell( 4717 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4718 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4719 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4720 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4721 ) 4722 4723 else: 4724 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4725 4726 elif args.order: 4727 if 4 <= len(args.order) <= 7: 4728 server.Order( 4729 operation=args.order[0], 4730 orderType=args.order[1], 4731 lots=int(args.order[2]), 4732 targetPrice=float(args.order[3]), 4733 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4734 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4735 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4736 ) 4737 4738 else: 4739 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4740 4741 elif args.buy_limit: 4742 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4743 4744 elif args.sell_limit: 4745 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4746 4747 elif args.buy_stop: 4748 if 2 <= len(args.buy_stop) <= 7: 4749 server.BuyStop( 4750 lots=int(args.buy_stop[0]), 4751 targetPrice=float(args.buy_stop[1]), 4752 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4753 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4754 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4755 ) 4756 4757 else: 4758 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4759 4760 elif args.sell_stop: 4761 if 2 <= len(args.sell_stop) <= 7: 4762 server.SellStop( 4763 lots=int(args.sell_stop[0]), 4764 targetPrice=float(args.sell_stop[1]), 4765 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4766 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4767 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4768 ) 4769 4770 else: 4771 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4772 4773 # elif args.buy_order_grid is not None: 4774 # # update order grid work with api v2 4775 # if len(args.buy_order_grid) == 2: 4776 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4777 # 4778 # for order in orderParams: 4779 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4780 # 4781 # else: 4782 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4783 # 4784 # elif args.sell_order_grid is not None: 4785 # # update order grid work with api v2 4786 # if len(args.sell_order_grid) >= 2: 4787 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4788 # 4789 # for order in orderParams: 4790 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4791 # 4792 # else: 4793 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4794 4795 elif args.close_order is not None: 4796 server.CloseOrders(args.close_order) # close only one order 4797 4798 elif args.close_orders is not None: 4799 server.CloseOrders(args.close_orders) # close list of orders 4800 4801 elif args.close_trade: 4802 if not args.ticker: 4803 uLogger.error("`--ticker` key is required for this operation!") 4804 raise Exception("Ticker required") 4805 4806 server.CloseTrades([args.ticker]) # close only one trade 4807 4808 elif args.close_trades is not None: 4809 server.CloseTrades(args.close_trades) # close trades for list of tickers 4810 4811 elif args.close_all is not None: 4812 server.CloseAll(*args.close_all) 4813 4814 elif args.limits: 4815 if args.output is not None: 4816 server.withdrawalLimitsFile = args.output 4817 4818 server.OverviewLimits(show=True) 4819 4820 elif args.user_info: 4821 if args.output is not None: 4822 server.userInfoFile = args.output 4823 4824 server.OverviewUserInfo(show=True) 4825 4826 elif args.account: 4827 if args.output is not None: 4828 server.userAccountsFile = args.output 4829 4830 server.OverviewAccounts(show=True) 4831 4832 else: 4833 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4834 raise Exception("There is no command to execute") 4835 4836 except Exception: 4837 trace = tb.format_exc() 4838 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4839 if e in trace: 4840 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4841 break 4842 4843 uLogger.debug(trace) 4844 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4845 exitCode = 255 # an error occurred, must be open a ticket for this issue 4846 4847 finally: 4848 finish = datetime.now(tzutc()) 4849 4850 if exitCode == 0: 4851 uLogger.debug("All operations were finished success (summary code is 0).") 4852 4853 else: 4854 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4855 os.path.abspath(uLog.defaultLogFile), exitCode, 4856 )) 4857 4858 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4859 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4860 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4861 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4862 )) 4863 4864 if not kwargs: 4865 sys.exit(exitCode) 4866 4867 else: 4868 return exitCode 4869 4870 4871if __name__ == "__main__": 4872 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """ 449 450 @staticmethod 451 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 452 """ 453 Parse JSON from response string. 454 455 :param rawData: this is a string with JSON-formatted text. 456 :param debug: if `True` then print more debug information. 457 :return: JSON (dictionary), parsed from server response string. 458 """ 459 if debug: 460 uLogger.debug("Raw text body:") 461 uLogger.debug(rawData) 462 463 responseJSON = json.loads(rawData) if rawData else {} 464 465 if debug: 466 uLogger.debug("JSON formatted:") 467 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 468 uLogger.debug(jsonLine) 469 470 return responseJSON 471 472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON 559 560 def _IUpdater(self, iType: str) -> tuple: 561 """ 562 Request instrument by type from server. See available API methods for instruments: 563 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 564 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 565 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 566 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 567 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 568 569 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 570 :return: tuple with iType name and list of available instruments of current type for defined user token. 571 """ 572 result = [] 573 574 if iType in TKS_INSTRUMENTS: 575 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 576 577 # all instruments have the same body in API v2 requests: 578 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 579 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 580 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 581 582 return iType, result 583 584 def _IWrapper(self, kwargs): 585 """ 586 Wrapper runs instrument's update method `_IUpdater()`. 587 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 588 """ 589 return self._IUpdater(**kwargs) 590 591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList 627 628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 668 669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump 694 695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText 936 937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON 1007 1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON 1103 1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1107 `{"buy": [{"price": 1243.8, "quantity": 193}, 1108 {"price": 1244.0, "quantity": 168}, 1109 {"price": 1244.8, "quantity": 5}, 1110 {"price": 1245.0, "quantity": 61}, 1111 {"price": 1245.4, "quantity": 60}], 1112 "sell": [{"price": 1243.6, "quantity": 8}, 1113 {"price": 1242.6, "quantity": 10}, 1114 {"price": 1242.4, "quantity": 18}, 1115 {"price": 1242.2, "quantity": 50}, 1116 {"price": 1242.0, "quantity": 113}], 1117 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1118 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1119 - sell: list of dicts with Buyers prices, 1120 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1121 - quantity: volume value by current price in lots, 1122 - limitUp: current trade session limit price, maximum, 1123 - limitDown: current trade session limit price, minimum, 1124 - lastPrice: last deal price of the instrument, 1125 - closePrice: previous trade session close price of the instrument. 1126 1127 See also: `SearchByTicker()` and `SearchByFIGI()`. 1128 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1129 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1130 1131 :param show: if `True` then print DOM to log and console. 1132 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1133 If an error occurred then returns an empty record: 1134 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1135 """ 1136 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1137 1138 if self.depth < 1: 1139 uLogger.error("Depth of Market (DOM) must be >=1!") 1140 raise Exception("Incorrect value") 1141 1142 if not (self.ticker or self.figi): 1143 uLogger.error("self.ticker or self.figi variables must be defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 if self.ticker and not self.figi: 1147 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1148 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1149 1150 if not self.ticker and self.figi: 1151 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1153 1154 if not self.figi: 1155 uLogger.error("FIGI is not defined!") 1156 raise Exception("Ticker or FIGI required") 1157 1158 else: 1159 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1160 1161 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1162 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1163 self.body = str({"figi": self.figi, "depth": self.depth}) 1164 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1165 1166 if pricesResponse: 1167 # list of dicts with sellers orders: 1168 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1169 1170 # list of dicts with buyers orders: 1171 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1172 1173 # max price of instrument at this time: 1174 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1175 1176 # min price of instrument at this time: 1177 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1178 1179 # last price of deal with instrument: 1180 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1181 1182 # last close price of instrument: 1183 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1184 1185 else: 1186 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1187 uLogger.debug("Server response: {}".format(pricesResponse)) 1188 1189 if show: 1190 if prices["buy"] or prices["sell"]: 1191 info = [ 1192 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1193 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1194 self.ticker, 1195 self.figi, 1196 self.depth, 1197 ), 1198 "-" * 60, "\n", 1199 " Orders of Buyers | Orders of Sellers\n", 1200 "-" * 60, "\n", 1201 " Sell prices (volumes) | Buy prices (volumes)\n", 1202 "-" * 60, "\n", 1203 ] 1204 1205 if not prices["buy"]: 1206 info.append(" | No orders!\n") 1207 sumBuy = 0 1208 1209 else: 1210 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1211 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1212 for item in maxMinSorted: 1213 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1214 1215 if not prices["sell"]: 1216 info.append("No orders! |\n") 1217 sumSell = 0 1218 1219 else: 1220 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1221 for item in prices["sell"]: 1222 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1223 1224 info.extend([ 1225 "-" * 60, "\n", 1226 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1227 "-" * 60, "\n", 1228 ]) 1229 1230 infoText = "".join(info) 1231 1232 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1233 1234 else: 1235 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1236 1237 return prices 1238 1239 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1240 """ 1241 This method get and show information about all available broker instruments for current user account. 1242 If `instrumentsFile` string is not empty then also save information to this file. 1243 1244 :param show: if `True` then print results to console, if `False` - print only to file. 1245 :return: multi-lines string with all available broker instruments 1246 """ 1247 if not self.iList: 1248 self.iList = self.Listing() 1249 1250 info = [ 1251 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1252 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1253 ] 1254 1255 # add instruments count by type: 1256 for iType in self.iList.keys(): 1257 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1258 1259 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1260 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1261 1262 # generating info tables with all instruments by type: 1263 for iType in self.iList.keys(): 1264 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1265 1266 for instrument in self.iList[iType].keys(): 1267 iName = self.iList[iType][instrument]["name"] # instrument's name 1268 if len(iName) > 57: 1269 iName = "{}...".format(iName[:54]) # right trim for a long string 1270 1271 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1272 self.iList[iType][instrument]["ticker"], 1273 iName, 1274 self.iList[iType][instrument]["figi"], 1275 self.iList[iType][instrument]["currency"], 1276 self.iList[iType][instrument]["lot"], 1277 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1278 )) 1279 1280 infoText = "".join(info) 1281 1282 if show: 1283 uLogger.info(infoText) 1284 1285 if self.instrumentsFile: 1286 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1287 fH.write(infoText) 1288 1289 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1290 1291 return infoText 1292 1293 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` - return list of result only. 1300 :return: list of dictionaries with all found instruments. 1301 """ 1302 if not self.iList: 1303 self.iList = self.Listing() 1304 1305 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1306 compiledPattern = re.compile(pattern, re.IGNORECASE) 1307 1308 for iType in self.iList: 1309 for instrument in self.iList[iType].values(): 1310 searchResult = compiledPattern.search(" ".join( 1311 [instrument["ticker"], instrument["figi"], instrument["name"]] 1312 )) 1313 1314 if searchResult: 1315 searchResults[iType][instrument["ticker"]] = instrument 1316 1317 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1318 info = [ 1319 "# Search results\n\n", 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile: 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 return searchResults 1371 1372 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1373 """ 1374 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1375 1376 :param instruments: list of strings with tickers or FIGIs. 1377 :return: list with unique instrument FIGIs only. 1378 """ 1379 requestedInstruments = [] 1380 for iName in instruments: 1381 if iName not in self.aliases.keys(): 1382 if iName not in requestedInstruments: 1383 requestedInstruments.append(iName) 1384 1385 else: 1386 if iName not in requestedInstruments: 1387 if self.aliases[iName] not in requestedInstruments: 1388 requestedInstruments.append(self.aliases[iName]) 1389 1390 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1391 1392 onlyUniqueFIGIs = [] 1393 for iName in requestedInstruments: 1394 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1395 continue 1396 1397 self.ticker = iName 1398 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1399 1400 if not iData: 1401 self.ticker = "" 1402 self.figi = iName 1403 1404 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1405 1406 if not iData: 1407 self.figi = "" 1408 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1409 1410 if iData and iData["figi"] not in onlyUniqueFIGIs: 1411 onlyUniqueFIGIs.append(iData["figi"]) 1412 1413 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1414 1415 return onlyUniqueFIGIs 1416 1417 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1418 """ 1419 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 If `pricesFile` string is not empty then also save information to this file. 1422 1423 :param instruments: list of strings with tickers or FIGIs. 1424 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1425 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1427 """ 1428 if instruments is None or not instruments: 1429 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1430 raise Exception("Ticker or FIGI required") 1431 1432 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1433 1434 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1435 1436 iList = [] # trying to get info and current prices about all unique instruments: 1437 for self.figi in onlyUniqueFIGIs: 1438 iData = self.SearchByFIGI(requestPrice=True) 1439 iList.append(iData) 1440 1441 self.ShowListOfPrices(iList, show) 1442 1443 return iList 1444 1445 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1446 """ 1447 Show table contains current prices of given instruments. 1448 1449 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1451 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1452 :return: multilines text in Markdown format as a table contains current prices. 1453 """ 1454 infoText = "" 1455 1456 if show or self.pricesFile: 1457 info = [ 1458 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1459 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1460 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1461 ] 1462 1463 for item in iList: 1464 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1465 item["ticker"], 1466 item["figi"], 1467 item["type"], 1468 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1469 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1470 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1471 "{} / {}".format( 1472 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1473 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1474 ), 1475 "{} / {}".format( 1476 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1477 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1478 ), 1479 item["currency"], 1480 )) 1481 1482 infoText = "".join(info) 1483 1484 if show: 1485 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1486 1487 if self.pricesFile: 1488 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1489 fH.write(infoText) 1490 1491 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1492 1493 return infoText 1494 1495 def RequestTradingStatus(self) -> dict: 1496 """ 1497 Requesting trading status for the instrument defined by `figi` variable. 1498 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1499 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1500 1501 :return: dictionary with trading status attributes. Response example: 1502 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1503 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1504 """ 1505 if self.figi is None or not self.figi: 1506 uLogger.error("Variable `figi` must be defined for using this method!") 1507 raise Exception("FIGI required") 1508 1509 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1510 1511 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1512 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1513 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1514 1515 uLogger.debug("Records about current trading status successfully received") 1516 1517 return tradingStatus 1518 1519 def RequestPortfolio(self) -> dict: 1520 """ 1521 Requesting actual user's portfolio for current `accountId`. 1522 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1524 1525 :return: dictionary with user's portfolio. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1535 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1536 1537 uLogger.debug("Records about user's portfolio successfully received") 1538 1539 return rawPortfolio 1540 1541 def RequestPositions(self) -> dict: 1542 """ 1543 Requesting open positions by currencies and instruments for current `accountId`. 1544 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1545 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1546 1547 :return: dictionary with open positions by instruments. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1557 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1558 1559 uLogger.debug("Records about current open positions successfully received") 1560 1561 return rawPositions 1562 1563 def RequestPendingOrders(self) -> list: 1564 """ 1565 Requesting current actual pending orders for current `accountId`. 1566 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1580 1581 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1582 1583 return rawOrders 1584 1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1602 1603 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1604 1605 return rawStopOrders 1606 1607 def Overview(self, show: bool = False, details: str = "full") -> dict: 1608 """ 1609 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1610 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1611 are defined then also save information to file. 1612 1613 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1614 many requests about the state of the portfolio, and then, based on the received data, a large number 1615 of calculation and statistics are collected. 1616 1617 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1618 :param details: how detailed should the information be? You should specify one of strings: 1619 `full` - shows full available information about portfolio status (by default), 1620 `positions` - shows only open positions, 1621 `digest` - show a short digest of the portfolio status, 1622 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1623 `orders` - shows only sections of open limits and stop orders. 1624 :return: dictionary with client's raw portfolio and some statistics. 1625 """ 1626 if self.accountId is None or not self.accountId: 1627 uLogger.error("Variable `accountId` must be defined for using this method!") 1628 raise Exception("Account ID required") 1629 1630 view = { 1631 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1632 "headers": {}, # list of dictionaries, response headers without "positions" section 1633 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1634 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1635 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1636 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1637 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1638 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1639 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1640 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1641 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1642 }, 1643 "stat": { # --- some statistics calculated using "raw" sections: 1644 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1645 "availableRUB": 0., # available rubles (without other currencies) 1646 "blockedRUB": 0., # blocked sum in Russian Rouble 1647 "totalChangesRUB": 0., # changes for all open trades in RUB 1648 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1649 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1650 "sharesCostRUB": 0., # costs of all shares in RUB 1651 "bondsCostRUB": 0., # costs of all bonds in RUB 1652 "etfsCostRUB": 0., # costs of all etfs in RUB 1653 "futuresCostRUB": 0., # costs of all futures in RUB 1654 "Currencies": [], # list of dictionaries of all currencies statistics 1655 "Shares": [], # list of dictionaries of all shares statistics 1656 "Bonds": [], # list of dictionaries of all bonds statistics 1657 "Etfs": [], # list of dictionaries of all etfs statistics 1658 "Futures": [], # list of dictionaries of all futures statistics 1659 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1660 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1661 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1662 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1663 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1664 }, 1665 "analytics": { # --- some analytics of portfolio: 1666 "distrByAssets": {}, # portfolio distribution by assets 1667 "distrByCompanies": {}, # portfolio distribution by companies 1668 "distrBySectors": {}, # portfolio distribution by sectors 1669 "distrByCurrencies": {}, # portfolio distribution by currencies 1670 "distrByCountries": {}, # portfolio distribution by countries 1671 } 1672 } 1673 1674 details = details.lower() 1675 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1676 if details not in availableDetails: 1677 details = "full" 1678 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1679 1680 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1681 1682 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1683 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1684 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1685 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1686 1687 # save response headers without "positions" section: 1688 for key in portfolioResponse.keys(): 1689 if key != "positions": 1690 view["raw"]["headers"][key] = portfolioResponse[key] 1691 1692 else: 1693 continue 1694 1695 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1696 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1697 for item in portfolioResponse["positions"]: 1698 if item["instrumentType"] == "currency": 1699 self.figi = item["figi"] 1700 curr = self.SearchByFIGI(requestPrice=False) 1701 1702 # current price of currency in RUB: 1703 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1704 "name": curr["name"], 1705 "currentPrice": NanoToFloat( 1706 item["currentPrice"]["units"], 1707 item["currentPrice"]["nano"] 1708 ), 1709 } 1710 1711 view["raw"]["Currencies"].append(item) 1712 1713 elif item["instrumentType"] == "share": 1714 view["raw"]["Shares"].append(item) 1715 1716 elif item["instrumentType"] == "bond": 1717 view["raw"]["Bonds"].append(item) 1718 1719 elif item["instrumentType"] == "etf": 1720 view["raw"]["Etfs"].append(item) 1721 1722 elif item["instrumentType"] == "futures": 1723 view["raw"]["Futures"].append(item) 1724 1725 else: 1726 continue 1727 1728 # how many volume of currencies (by ISO currency name) are blocked: 1729 for item in view["raw"]["positions"]["blocked"]: 1730 blocked = NanoToFloat(item["units"], item["nano"]) 1731 if blocked > 0: 1732 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1733 1734 # how many volume of instruments (by FIGI) are blocked: 1735 for item in view["raw"]["positions"]["securities"]: 1736 blocked = int(item["blocked"]) 1737 if blocked > 0: 1738 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1739 1740 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1741 1742 if "rub" in allBlocked.keys(): 1743 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1744 1745 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1746 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1747 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1748 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1749 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1750 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1751 view["stat"]["portfolioCostRUB"] = sum([ 1752 view["stat"]["allCurrenciesCostRUB"], 1753 view["stat"]["sharesCostRUB"], 1754 view["stat"]["bondsCostRUB"], 1755 view["stat"]["etfsCostRUB"], 1756 view["stat"]["futuresCostRUB"], 1757 ]) 1758 1759 # --- calculating some portfolio statistics: 1760 byComp = {} # distribution by companies 1761 bySect = {} # distribution by sectors 1762 byCurr = {} # distribution by currencies (include RUB) 1763 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1764 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1765 1766 for item in portfolioResponse["positions"]: 1767 self.figi = item["figi"] 1768 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1769 1770 if instrument: 1771 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1772 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1773 1774 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1775 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1776 1777 else: 1778 blocked = 0 1779 1780 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1781 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1782 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1783 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1784 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1785 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1786 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1787 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1788 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1789 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1790 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1791 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1792 1793 statData = { 1794 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1795 "ticker": instrument["ticker"], # ticker by FIGI 1796 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1797 "volume": volume, # available volume of instrument 1798 "lots": lots, # volume in lots of instrument 1799 "direction": direction, # direction of an instrument's position: short or long 1800 "blocked": blocked, # blocked volume of currency or instrument 1801 "currentPrice": curPrice, # current instrument's price in basic asset 1802 "average": average, # current average position price 1803 "cost": cost, # current cost of all volume of instrument in basic asset 1804 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1805 "costRUB": costRUB, # cost of instrument in ruble 1806 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1807 "profit": profit, # expected profit at current moment 1808 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1809 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1810 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1811 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1812 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1813 "step": instrument["step"], # minimum price increment 1814 } 1815 1816 # adding distribution by unique countries: 1817 if statData["country"] not in byCountry.keys(): 1818 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1819 1820 else: 1821 byCountry[statData["country"]]["cost"] += costRUB 1822 byCountry[statData["country"]]["percent"] += percentCostRUB 1823 1824 if item["instrumentType"] != "currency": 1825 # adding distribution by unique companies: 1826 if statData["name"]: 1827 if statData["name"] not in byComp.keys(): 1828 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 byComp[statData["name"]]["cost"] += costRUB 1832 byComp[statData["name"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique sectors: 1835 if statData["sector"] not in bySect.keys(): 1836 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 bySect[statData["sector"]]["cost"] += costRUB 1840 bySect[statData["sector"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique currencies: 1843 if currency not in byCurr.keys(): 1844 byCurr[currency] = { 1845 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1846 "cost": costRUB, 1847 "percent": percentCostRUB 1848 } 1849 1850 else: 1851 byCurr[currency]["cost"] += costRUB 1852 byCurr[currency]["percent"] += percentCostRUB 1853 1854 # saving statistics for every instrument: 1855 if item["instrumentType"] == "currency": 1856 view["stat"]["Currencies"].append(statData) 1857 1858 # update dict with free funds for trading (total - blocked) by currencies 1859 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1860 view["stat"]["funds"][currency] = { 1861 "total": volume, 1862 "totalCostRUB": costRUB, # total volume cost in rubles 1863 "free": volume - blocked, 1864 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1865 } 1866 1867 elif item["instrumentType"] == "share": 1868 view["stat"]["Shares"].append(statData) 1869 1870 elif item["instrumentType"] == "bond": 1871 view["stat"]["Bonds"].append(statData) 1872 1873 elif item["instrumentType"] == "etf": 1874 view["stat"]["Etfs"].append(statData) 1875 1876 elif item["instrumentType"] == "Futures": 1877 view["stat"]["Futures"].append(statData) 1878 1879 else: 1880 continue 1881 1882 # total changes in Russian Ruble: 1883 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1884 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1885 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1886 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1887 view["stat"]["funds"]["rub"] = { 1888 "total": view["stat"]["availableRUB"], 1889 "totalCostRUB": view["stat"]["availableRUB"], 1890 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1892 } 1893 1894 # --- pending orders sector data: 1895 uniquePendingOrders = [] 1896 uniquePendingOrdersFIGIs = [] 1897 for item in view["raw"]["orders"]: 1898 if item["figi"] not in uniquePendingOrdersFIGIs: 1899 uniquePendingOrdersFIGIs.append(item["figi"]) 1900 uniquePendingOrders.append(item) 1901 1902 for item in uniquePendingOrders: 1903 self.figi = item["figi"] 1904 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrders = [] 1944 uniqueStopOrdersFIGIs = [] 1945 for item in view["raw"]["stopOrders"]: 1946 if item["figi"] not in uniqueStopOrdersFIGIs: 1947 uniqueStopOrdersFIGIs.append(item["figi"]) 1948 uniqueStopOrders.append(item) 1949 1950 for item in uniqueStopOrders: 1951 self.figi = item["figi"] 1952 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2055 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2056 2057 view["analytics"]["distrByCurrencies"].update(byCurr) 2058 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2059 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2060 2061 # portfolio distribution by countries: 2062 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2063 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2065 2066 view["analytics"]["distrByCountries"].update(byCountry) 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2068 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2069 2070 # --- Prepare text statistics overview in human-readable: 2071 if show: 2072 # Whatever the value `details`, header not changes: 2073 info = [ 2074 "# Client's portfolio\n\n", 2075 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2076 "* **Account ID:** [{}]\n".format(self.accountId), 2077 ] 2078 2079 if details in ["full", "positions", "digest"]: 2080 info.extend([ 2081 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2082 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2083 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2084 view["stat"]["totalChangesRUB"], 2085 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2086 view["stat"]["totalChangesPercentRUB"], 2087 ), 2088 ]) 2089 2090 if details in ["full", "positions"]: 2091 info.extend([ 2092 "## Open positions\n\n", 2093 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2094 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2095 "| Ruble | {:>31} | | | | | |\n".format( 2096 "{:.2f} ({:.2f}) rub".format( 2097 view["stat"]["availableRUB"], 2098 view["stat"]["blockedRUB"], 2099 ) 2100 ) 2101 ]) 2102 2103 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2104 return [ 2105 "| | | | | | | |\n", 2106 "| {:<27} | | | | | {:>19} | |\n".format( 2107 noTradeStr if noTradeStr else typeStr, 2108 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2109 ), 2110 ] 2111 2112 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2113 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2114 "{} [{}]".format(data["ticker"], data["figi"]), 2115 "{:.2f} ({:.2f}) {}".format( 2116 data["volume"], 2117 data["blocked"], 2118 data["currency"], 2119 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2120 data["volume"], 2121 data["blocked"], 2122 ), 2123 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2124 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2126 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2127 "{}{:.2f} {} ({}{:.2f}%)".format( 2128 "+" if data["profit"] > 0 else "", 2129 data["profit"], data["baseCurrencyName"], 2130 "+" if data["percentProfit"] > 0 else "", 2131 data["percentProfit"], 2132 ), 2133 ) 2134 2135 # --- Show currencies section: 2136 if view["stat"]["Currencies"]: 2137 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2138 for item in view["stat"]["Currencies"]: 2139 info.append(_InfoStr(item, showCurrencyName=True)) 2140 2141 else: 2142 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2143 2144 # --- Show shares section: 2145 if view["stat"]["Shares"]: 2146 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2147 2148 for item in view["stat"]["Shares"]: 2149 info.append(_InfoStr(item)) 2150 2151 else: 2152 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2153 2154 # --- Show bonds section: 2155 if view["stat"]["Bonds"]: 2156 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2157 2158 for item in view["stat"]["Bonds"]: 2159 info.append(_InfoStr(item)) 2160 2161 else: 2162 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2163 2164 # --- Show etfs section: 2165 if view["stat"]["Etfs"]: 2166 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2167 2168 for item in view["stat"]["Etfs"]: 2169 info.append(_InfoStr(item)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2173 2174 # --- Show futures section: 2175 if view["stat"]["Futures"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2177 2178 for item in view["stat"]["Futures"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2183 2184 if details in ["full", "orders"]: 2185 # --- Show pending orders section: 2186 if view["stat"]["orders"]: 2187 info.extend([ 2188 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2189 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2190 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2191 ]) 2192 2193 for item in view["stat"]["orders"]: 2194 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2195 "{} [{}]".format(item["ticker"], item["figi"]), 2196 item["orderID"], 2197 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2198 "{} {} ({}{:.2f}%)".format( 2199 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2200 item["baseCurrencyName"], 2201 "+" if item["percentChanges"] > 0 else "", 2202 float(item["percentChanges"]), 2203 ), 2204 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2205 item["action"], 2206 item["type"], 2207 item["date"], 2208 )) 2209 2210 else: 2211 info.append("\n## Total pending limit-orders: 0\n") 2212 2213 # --- Show stop orders section: 2214 if view["stat"]["stopOrders"]: 2215 info.extend([ 2216 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2217 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2218 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["stopOrders"]: 2222 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 item["lotsRequested"], 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2234 item["action"], 2235 item["type"], 2236 item["expType"], 2237 item["createDate"], 2238 item["expDate"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total stop-orders: 0\n") 2243 2244 if details in ["full", "analytics"]: 2245 # -- Show analytics section: 2246 if view["stat"]["portfolioCostRUB"] > 0: 2247 info.extend([ 2248 "\n# Analytics\n" 2249 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2250 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2251 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2252 view["stat"]["totalChangesRUB"], 2253 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2254 view["stat"]["totalChangesPercentRUB"], 2255 ), 2256 "\n## Portfolio distribution by assets\n" 2257 "\n| Type | Uniques | Percent | Current cost |\n", 2258 "|------------|---------|---------|--------------------|\n", 2259 ]) 2260 2261 for key in view["analytics"]["distrByAssets"].keys(): 2262 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2263 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2264 key, 2265 view["analytics"]["distrByAssets"][key]["uniques"], 2266 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2267 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2268 )) 2269 2270 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2271 info.extend([ 2272 "\n## Portfolio distribution by companies\n" 2273 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2274 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2275 ]) 2276 2277 for company in view["analytics"]["distrByCompanies"].keys(): 2278 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2279 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2280 info.append("| {} | {:<7} | {:<18} |\n".format( 2281 "{}{}{}".format( 2282 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2283 company, 2284 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2285 ), 2286 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2288 )) 2289 2290 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2291 info.extend([ 2292 "\n## Portfolio distribution by sectors\n" 2293 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2294 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2295 ]) 2296 2297 for sector in view["analytics"]["distrBySectors"].keys(): 2298 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2299 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2300 sector, 2301 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2302 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2304 )) 2305 2306 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2307 info.extend([ 2308 "\n## Portfolio distribution by currencies\n" 2309 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2310 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2311 ]) 2312 2313 for curr in view["analytics"]["distrByCurrencies"].keys(): 2314 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2315 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2316 info.append("| {} | {:<7} | {:<18} |\n".format( 2317 "[{}] {}{}".format( 2318 curr, 2319 view["analytics"]["distrByCurrencies"][curr]["name"], 2320 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2324 )) 2325 2326 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2327 info.extend([ 2328 "\n## Portfolio distribution by countries\n" 2329 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2330 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2331 ]) 2332 2333 for country in view["analytics"]["distrByCountries"].keys(): 2334 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2335 nameLen = len(country) 2336 info.append("| {} | {:<7} | {:<18} |\n".format( 2337 "{}{}".format( 2338 country, 2339 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2340 ), 2341 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2343 )) 2344 2345 infoText = "".join(info) 2346 2347 uLogger.info(infoText) 2348 2349 if details == "full" and self.overviewFile: 2350 filename = self.overviewFile 2351 2352 elif details == "digest" and self.overviewDigestFile: 2353 filename = self.overviewDigestFile 2354 2355 elif details == "positions" and self.overviewPositionsFile: 2356 filename = self.overviewPositionsFile 2357 2358 elif details == "orders" and self.overviewOrdersFile: 2359 filename = self.overviewOrdersFile 2360 2361 elif details == "analytics" and self.overviewAnalyticsFile: 2362 filename = self.overviewAnalyticsFile 2363 2364 else: 2365 filename = "" 2366 2367 if filename: 2368 with open(filename, "w", encoding="UTF-8") as fH: 2369 fH.write(infoText) 2370 2371 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2372 2373 return view 2374 2375 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2376 """ 2377 Returns history operations between two given dates for current `accountId`. 2378 If `reportFile` string is not empty then also save human-readable report. 2379 Shows some statistical data of closed positions. 2380 2381 :param start: see docstring in `GetDatesAsString()` method 2382 :param end: see docstring in `GetDatesAsString()` method 2383 :param show: if `True` then also prints all records to the console. 2384 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2385 :return: original list of dictionaries with history of deals records from API ("operations" key): 2386 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2387 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2388 """ 2389 if self.accountId is None or not self.accountId: 2390 uLogger.error("Variable `accountId` must be defined for using this method!") 2391 raise Exception("Account ID required") 2392 2393 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2394 2395 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2396 2397 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2398 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2399 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2400 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2401 customStat = {} # custom statistics in additional to responseJSON 2402 2403 # --- output report in human-readable format: 2404 if show or self.reportFile: 2405 splitLine1 = "| | | | | |\n" # Summary section 2406 splitLine2 = "| | | | | | | | |\n" # Operations section 2407 nextDay = "" 2408 2409 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2410 2411 if len(ops) > 0: 2412 customStat = { 2413 "opsCount": 0, # total operations count 2414 "buyCount": 0, # buy operations 2415 "sellCount": 0, # sell operations 2416 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2417 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2418 "payIn": {"rub": 0.}, # Deposit brokerage account 2419 "payOut": {"rub": 0.}, # Withdrawals 2420 "divs": {"rub": 0.}, # Dividends income 2421 "coupons": {"rub": 0.}, # Coupon's income 2422 "brokerCom": {"rub": 0.}, # Service commissions 2423 "serviceCom": {"rub": 0.}, # Service commissions 2424 "marginCom": {"rub": 0.}, # Margin commissions 2425 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2426 } 2427 2428 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2429 for item in ops: 2430 if item["state"] == "OPERATION_STATE_EXECUTED": 2431 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2432 2433 # count buy operations: 2434 if "_BUY" in item["operationType"]: 2435 customStat["buyCount"] += 1 2436 2437 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2438 customStat["buyTotal"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["buyTotal"][item["payment"]["currency"]] = payment 2442 2443 # count sell operations: 2444 elif "_SELL" in item["operationType"]: 2445 customStat["sellCount"] += 1 2446 2447 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2448 customStat["sellTotal"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["sellTotal"][item["payment"]["currency"]] = payment 2452 2453 # count incoming operations: 2454 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2455 if item["payment"]["currency"] in customStat["payIn"].keys(): 2456 customStat["payIn"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["payIn"][item["payment"]["currency"]] = payment 2460 2461 # count withdrawals operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2463 if item["payment"]["currency"] in customStat["payOut"].keys(): 2464 customStat["payOut"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payOut"][item["payment"]["currency"]] = payment 2468 2469 # count dividends income: 2470 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2471 if item["payment"]["currency"] in customStat["divs"].keys(): 2472 customStat["divs"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["divs"][item["payment"]["currency"]] = payment 2476 2477 # count coupon's income: 2478 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2479 if item["payment"]["currency"] in customStat["coupons"].keys(): 2480 customStat["coupons"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["coupons"][item["payment"]["currency"]] = payment 2484 2485 # count broker commissions: 2486 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2487 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2488 customStat["brokerCom"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["brokerCom"][item["payment"]["currency"]] = payment 2492 2493 # count service commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2495 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2496 customStat["serviceCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["serviceCom"][item["payment"]["currency"]] = payment 2500 2501 # count margin commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2503 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2504 customStat["marginCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["marginCom"][item["payment"]["currency"]] = payment 2508 2509 # count withholding taxes: 2510 elif "_TAX" in item["operationType"]: 2511 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2512 customStat["allTaxes"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["allTaxes"][item["payment"]["currency"]] = payment 2516 2517 else: 2518 continue 2519 2520 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2521 2522 # --- view "Actions" lines: 2523 info.extend([ 2524 "| 1 | 2 | 3 | 4 | 5 |\n", 2525 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2526 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2527 "| | Buy: {:<22} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2530 ), 2531 "| | Sell: {:<21} | {:<28} | | |\n".format( 2532 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2533 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2534 ), 2535 ]) 2536 2537 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2538 for key in opsKeys: 2539 if key == "rub": 2540 continue 2541 2542 info.extend([ 2543 "| | | {:<28} | | |\n".format( 2544 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2545 ), 2546 "| | | {:<28} | | |\n".format( 2547 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2548 ), 2549 ]) 2550 2551 info.append(splitLine1) 2552 2553 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2554 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2555 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2558 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2559 ) 2560 2561 # --- view "Payments" lines: 2562 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2563 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2564 2565 for key in paymentsKeys: 2566 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2567 2568 info.append(splitLine1) 2569 2570 # --- view "Commissions and taxes" lines: 2571 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2572 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2573 2574 for key in comKeys: 2575 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2576 2577 info.append(splitLine1) 2578 2579 info.extend([ 2580 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2581 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2582 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2583 ]) 2584 2585 else: 2586 info.append("Broker returned no operations during this period\n") 2587 2588 # --- view "Operations" section: 2589 for item in ops: 2590 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2591 continue 2592 2593 else: 2594 self.figi = item["figi"] if item["figi"] else "" 2595 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2596 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2597 2598 # group of deals during one day: 2599 if nextDay and item["date"].split("T")[0] != nextDay: 2600 info.append(splitLine2) 2601 nextDay = "" 2602 2603 else: 2604 nextDay = item["date"].split("T")[0] # saving current day for splitting 2605 2606 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2607 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2608 self.figi if self.figi else "—", 2609 instrument["ticker"] if instrument else "—", 2610 instrument["type"] if instrument else "—", 2611 item["quantity"] if int(item["quantity"]) > 0 else "—", 2612 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2613 TKS_OPERATION_STATES[item["state"]], 2614 TKS_OPERATION_TYPES[item["operationType"]], 2615 )) 2616 2617 infoText = "".join(info) 2618 2619 if show: 2620 uLogger.info(infoText) 2621 2622 if self.reportFile: 2623 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2624 fH.write(infoText) 2625 2626 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2627 2628 return ops, customStat 2629 2630 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2631 """ 2632 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2633 2634 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2635 Warning! Broker server used ISO UTC time by default. 2636 2637 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2638 Also, `historyFile` used to update history with `onlyMissing` parameter. 2639 2640 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2641 2642 :param start: see docstring in `GetDatesAsString()` method. 2643 :param end: see docstring in `GetDatesAsString()` method. 2644 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2645 `"hour"`, `"day"`. Default: `"hour"`. 2646 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2647 False by default. Warning! History appends only from last candle to current time 2648 with always update last candle! 2649 :param csvSep: separator if csv-file is used, `,` by default. 2650 :param show: if `True` then also prints Pandas DataFrame to the console. 2651 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2652 `["date", "time", "open", "high", "low", "close", "volume"]`. 2653 """ 2654 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2655 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2656 history = None # empty pandas object for history 2657 2658 if interval not in TKS_CANDLE_INTERVALS.keys(): 2659 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2660 raise Exception("Incorrect value") 2661 2662 if not (self.ticker or self.figi): 2663 uLogger.error("Ticker or FIGI must be defined!") 2664 raise Exception("Ticker or FIGI required") 2665 2666 if self.ticker and not self.figi: 2667 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2668 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2669 2670 if self.figi and not self.ticker: 2671 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2672 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2673 2674 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2675 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2676 if interval.lower() != "day": 2677 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2678 2679 delta = dtEnd - dtStart # current UTC time minus last time in file 2680 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2681 2682 # calculate history length in candles: 2683 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2684 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2685 length += 1 # to avoid fraction time 2686 2687 # calculate data blocks count: 2688 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2689 2690 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2691 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2692 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2693 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2694 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2695 2696 tempOld = None # pandas object for old history, if --only-missing key present 2697 lastTime = None # datetime object of last old candle in file 2698 2699 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2700 uLogger.debug("--only-missing key present, add only last missing candles...") 2701 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2702 2703 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2704 2705 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2706 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2707 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2708 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2709 2710 # get last datetime object from last string in file or minus 1 delta if file is empty: 2711 if len(tempOld) > 0: 2712 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2713 2714 else: 2715 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2716 2717 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2718 2719 responseJSONs = [] # raw history blocks of data 2720 2721 blockEnd = dtEnd 2722 for item in range(blocks): 2723 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2724 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2725 2726 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2727 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2728 )) 2729 2730 if blockStart == blockEnd: 2731 uLogger.debug("Skipped this zero-length block...") 2732 2733 else: 2734 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2735 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2736 self.body = str({ 2737 "figi": self.figi, 2738 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2739 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2740 "interval": TKS_CANDLE_INTERVALS[interval][0] 2741 }) 2742 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2743 2744 if "code" in responseJSON.keys(): 2745 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2746 2747 else: 2748 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2749 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2750 2751 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2752 2753 blockEnd = blockStart 2754 2755 printCount = len(responseJSONs) # candles to show in console 2756 if responseJSONs: 2757 tempHistory = pd.DataFrame( 2758 data={ 2759 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2761 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2762 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2763 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2764 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2765 "volume": [int(item["volume"]) for item in responseJSONs], 2766 }, 2767 index=range(len(responseJSONs)), 2768 columns=["date", "time", "open", "high", "low", "close", "volume"], 2769 ) 2770 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2771 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2772 2773 # append only newest candles to old history if --only-missing key present: 2774 if onlyMissing and tempOld is not None and lastTime is not None: 2775 index = 0 # find start index in tempHistory data: 2776 2777 for i, item in tempHistory.iterrows(): 2778 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2779 2780 if curTime == lastTime: 2781 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2782 index = i 2783 printCount = index + 1 2784 break 2785 2786 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2787 2788 else: 2789 history = tempHistory # if no `--only-missing` key then load full data from server 2790 2791 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2792 2793 if history is not None and not history.empty: 2794 if show: 2795 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2796 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2797 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2798 )) 2799 2800 else: 2801 uLogger.warning("Received an empty candles history!") 2802 2803 if self.historyFile is not None: 2804 if history is not None and not history.empty: 2805 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2806 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2807 2808 else: 2809 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2810 2811 else: 2812 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2813 2814 return history 2815 2816 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2817 """ 2818 Load candles history from csv-file and return Pandas DataFrame object. 2819 2820 See also: `History()` and `ShowHistoryChart()` methods. 2821 2822 :param filePath: path to csv-file to open. 2823 """ 2824 loadedHistory = None # init candles data object 2825 2826 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2827 2828 if os.path.exists(filePath): 2829 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2830 2831 tfStr = self.priceModel.FormattedDelta( 2832 self.priceModel.timeframe, 2833 "{days} days {hours}h {minutes}m {seconds}s", 2834 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2835 self.priceModel.timeframe, 2836 "{hours}h {minutes}m {seconds}s", 2837 ) 2838 2839 if loadedHistory is not None and not loadedHistory.empty: 2840 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2841 len(loadedHistory), 2842 tfStr, 2843 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2844 ) 2845 2846 else: 2847 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2848 2849 else: 2850 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2851 2852 return loadedHistory 2853 2854 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2855 """ 2856 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2857 2858 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2859 Default: `index.html` (both for interact and non-interact candlesticks chart). 2860 2861 See also: `History()` and `LoadHistory()` methods. 2862 2863 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2864 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2865 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2866 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2867 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2868 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2869 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2870 """ 2871 if isinstance(candles, str): 2872 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2873 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2874 2875 elif isinstance(candles, pd.DataFrame): 2876 self.priceModel.prices = candles # set candles chain from variable 2877 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2878 2879 if "datetime" not in candles.columns: 2880 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2881 2882 else: 2883 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2884 raise Exception("Incorrect value") 2885 2886 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2887 2888 if interact: 2889 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2890 2891 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2892 2893 else: 2894 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2895 2896 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2897 2898 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2899 2900 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2901 """ 2902 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2903 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2904 2905 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2906 2907 :param operation: string "Buy" or "Sell". 2908 :param lots: volume, integer count of lots >= 1. 2909 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2910 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2911 :param expDate: string "Undefined" by default or local date in future, 2912 it is a string with format `%Y-%m-%d %H:%M:%S`. 2913 :return: JSON with response from broker server. 2914 """ 2915 if self.accountId is None or not self.accountId: 2916 uLogger.error("Variable `accountId` must be defined for using this method!") 2917 raise Exception("Account ID required") 2918 2919 if operation is None or not operation or operation not in ("Buy", "Sell"): 2920 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2921 raise Exception("Incorrect value") 2922 2923 if lots is None or lots < 1: 2924 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2925 lots = 1 2926 2927 if tp is None or tp < 0: 2928 tp = 0 2929 2930 if sl is None or sl < 0: 2931 sl = 0 2932 2933 if expDate is None or not expDate: 2934 expDate = "Undefined" 2935 2936 if not (self.ticker or self.figi): 2937 uLogger.error("Ticker or FIGI must be defined!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2941 self.ticker = instrument["ticker"] 2942 self.figi = instrument["figi"] 2943 2944 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2945 2946 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2947 self.body = str({ 2948 "figi": self.figi, 2949 "quantity": str(lots), 2950 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2951 "accountId": str(self.accountId), 2952 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2953 }) 2954 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2955 2956 if "orderId" in response.keys(): 2957 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2958 operation, response["orderId"], 2959 self.ticker, self.figi, lots, 2960 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2961 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2962 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2963 )) 2964 2965 else: 2966 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2967 2968 if tp > 0: 2969 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2970 2971 if sl > 0: 2972 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2973 2974 return response 2975 2976 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2977 """ 2978 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2979 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2980 2981 See also: `Order()` and `Trade()` docstrings. 2982 2983 :param lots: volume, integer count of lots >= 1. 2984 :param tp: float > 0, take profit price of stop-order. 2985 :param sl: float > 0, stop loss price of stop-order. 2986 :param expDate: it's a local date in future. 2987 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2988 :return: JSON with response from broker server. 2989 """ 2990 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2991 2992 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` and `Trade()` docstrings. 2998 2999 :param lots: volume, integer count of lots >= 1. 3000 :param tp: float > 0, take profit price of stop-order. 3001 :param sl: float > 0, stop loss price of stop-order. 3002 :param expDate: it's a local date in the future. 3003 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3004 :return: JSON with response from broker server. 3005 """ 3006 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3007 3008 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 3009 """ 3010 Close position of given instruments. 3011 3012 :param tickers: tickers list of instruments that must be closed. 3013 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3014 This avoids unnecessary downloading data from the server. 3015 """ 3016 if not tickers: 3017 uLogger.info("Tickers list is empty, nothing to close.") 3018 3019 else: 3020 if portfolio is None or not portfolio: 3021 portfolio = self.Overview(show=False) 3022 3023 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3024 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3025 3026 for ticker in tickers: 3027 if ticker not in allOpenedTickers: 3028 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3029 continue 3030 3031 # search open trade info about instrument by ticker: 3032 instrument = {} 3033 for iType in TKS_INSTRUMENTS: 3034 if instrument: 3035 break 3036 3037 for item in portfolio["stat"][iType]: 3038 if item["ticker"] == ticker: 3039 instrument = item 3040 break 3041 3042 if instrument: 3043 self.ticker = ticker 3044 self.figi = instrument["figi"] 3045 3046 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3047 self.ticker, 3048 self.figi, 3049 int(instrument["volume"]), 3050 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3051 )) 3052 3053 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3054 3055 if tradeLots > 0: 3056 if instrument["blocked"] > 0: 3057 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3058 instrument["blocked"], 3059 self.ticker, 3060 tradeLots, 3061 )) 3062 3063 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3064 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3065 3066 else: 3067 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3068 3069 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3070 """ 3071 Close all positions of given instruments with defined type. 3072 3073 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3074 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3075 This avoids unnecessary downloading data from the server. 3076 """ 3077 if iType not in TKS_INSTRUMENTS: 3078 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3079 3080 else: 3081 if portfolio is None or not portfolio: 3082 portfolio = self.Overview(show=False) 3083 3084 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3085 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3086 3087 if tickers and portfolio: 3088 self.CloseTrades(tickers, portfolio) 3089 3090 else: 3091 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3092 3093 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3094 """ 3095 Universal method to create market or limit orders with all available parameters for current `accountId`. 3096 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3097 3098 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3099 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3100 3101 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3102 then broker immediately open market order as you can do simple --buy or --sell operations! 3103 3104 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3105 When current price will go up or down to target price value then broker opens a limit order. 3106 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3107 3108 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3109 3110 :param operation: string "Buy" or "Sell". 3111 :param orderType: string "Limit" or "Stop". 3112 :param lots: volume, integer count of lots >= 1. 3113 :param targetPrice: target price > 0. This is open trade price for limit order. 3114 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3115 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3116 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3117 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3118 Stop loss order always executed by market price. 3119 :param expDate: string "Undefined" by default or local date in future. 3120 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3121 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3122 A limit order has no expiration date, it lasts until the end of the trading day. 3123 :return: JSON with response from broker server. 3124 """ 3125 if self.accountId is None or not self.accountId: 3126 uLogger.error("Variable `accountId` must be defined for using this method!") 3127 raise Exception("Account ID required") 3128 3129 if operation is None or not operation or operation not in ("Buy", "Sell"): 3130 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3131 raise Exception("Incorrect value") 3132 3133 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3134 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3135 raise Exception("Incorrect value") 3136 3137 if lots is None or lots < 1: 3138 uLogger.error("You must define trade volume > 0: integer count of lots!") 3139 raise Exception("Incorrect value") 3140 3141 if targetPrice is None or targetPrice <= 0: 3142 uLogger.error("Target price for limit-order must be greater than 0!") 3143 raise Exception("Incorrect value") 3144 3145 if limitPrice is None or limitPrice <= 0: 3146 limitPrice = targetPrice 3147 3148 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3149 stopType = "Limit" 3150 3151 if expDate is None or not expDate: 3152 expDate = "Undefined" 3153 3154 if not (self.ticker or self.figi): 3155 uLogger.error("Tocker or FIGI must be defined!") 3156 raise Exception("Ticker or FIGI required") 3157 3158 response = {} 3159 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3160 self.ticker = instrument["ticker"] 3161 self.figi = instrument["figi"] 3162 3163 if orderType == "Limit": 3164 uLogger.debug( 3165 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3166 self.ticker, self.figi, 3167 operation, lots, targetPrice, instrument["currency"], 3168 )) 3169 3170 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3171 self.body = str({ 3172 "figi": self.figi, 3173 "quantity": str(lots), 3174 "price": FloatToNano(targetPrice), 3175 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3176 "accountId": str(self.accountId), 3177 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3178 }) 3179 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3180 3181 if "orderId" in response.keys(): 3182 uLogger.info( 3183 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3184 response["orderId"], 3185 self.ticker, self.figi, 3186 operation, lots, targetPrice, instrument["currency"], 3187 )) 3188 3189 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3190 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3191 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3197 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3198 targetPrice, instrument["currency"], 3199 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3200 )) 3201 3202 else: 3203 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3204 3205 if orderType == "Stop": 3206 uLogger.debug( 3207 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3208 self.ticker, self.figi, 3209 operation, lots, 3210 targetPrice, instrument["currency"], 3211 limitPrice, instrument["currency"], 3212 stopType, expDate, 3213 )) 3214 3215 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3216 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3217 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3218 3219 body = { 3220 "figi": self.figi, 3221 "quantity": str(lots), 3222 "price": FloatToNano(limitPrice), 3223 "stopPrice": FloatToNano(targetPrice), 3224 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3225 "accountId": str(self.accountId), 3226 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3227 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3228 } 3229 3230 if expDateUTC: 3231 body["expireDate"] = expDateUTC 3232 3233 self.body = str(body) 3234 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3235 3236 if "stopOrderId" in response.keys(): 3237 uLogger.info( 3238 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3239 response["stopOrderId"], 3240 self.ticker, self.figi, 3241 operation, lots, 3242 targetPrice, instrument["currency"], 3243 limitPrice, instrument["currency"], 3244 TKS_STOP_ORDER_TYPES[stopOrderType], 3245 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3246 )) 3247 3248 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3249 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3250 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3256 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 else: 3262 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3263 3264 return response 3265 3266 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3267 """ 3268 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3269 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3270 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3271 See also: `Order()` docstring. 3272 3273 :param lots: volume, integer count of lots >= 1. 3274 :param targetPrice: target price > 0. This is open trade price for limit order. 3275 :return: JSON with response from broker server. 3276 """ 3277 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3278 3279 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3280 """ 3281 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3282 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3283 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3284 target price value then broker opens a limit order. See also: `Order()` docstring. 3285 3286 :param lots: volume, integer count of lots >= 1. 3287 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3288 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3289 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3290 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3291 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3292 :param expDate: string "Undefined" by default or local date in future. 3293 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3294 This date is converting to UTC format for server. 3295 :return: JSON with response from broker server. 3296 """ 3297 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3298 3299 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3300 """ 3301 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3302 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3303 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3304 See also: `Order()` docstring. 3305 3306 :param lots: volume, integer count of lots >= 1. 3307 :param targetPrice: target price > 0. This is open trade price for limit order. 3308 :return: JSON with response from broker server. 3309 """ 3310 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3311 3312 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3313 """ 3314 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3315 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3316 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3317 target price value then broker opens a limit order. See also: `Order()` docstring. 3318 3319 :param lots: volume, integer count of lots >= 1. 3320 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3321 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3322 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3323 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3324 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3325 :param expDate: string "Undefined" by default or local date in future. 3326 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3327 This date is converting to UTC format for server. 3328 :return: JSON with response from broker server. 3329 """ 3330 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3331 3332 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3333 """ 3334 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3335 3336 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3337 :param allOrdersIDs: pre-received lists of all active pending orders. 3338 This avoids unnecessary downloading data from the server. 3339 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3340 """ 3341 if self.accountId is None or not self.accountId: 3342 uLogger.error("Variable `accountId` must be defined for using this method!") 3343 raise Exception("Account ID required") 3344 3345 if orderIDs: 3346 if allOrdersIDs is None or not allOrdersIDs: 3347 rawOrders = self.RequestPendingOrders() 3348 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3349 3350 if allStopOrdersIDs is None or not allStopOrdersIDs: 3351 rawStopOrders = self.RequestStopOrders() 3352 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3353 3354 for orderID in orderIDs: 3355 idInPendingOrders = orderID in allOrdersIDs 3356 idInStopOrders = orderID in allStopOrdersIDs 3357 3358 if not (idInPendingOrders or idInStopOrders): 3359 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3360 continue 3361 3362 else: 3363 if idInPendingOrders: 3364 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3365 3366 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3367 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3368 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3369 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3370 3371 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3372 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3373 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3374 3375 else: 3376 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3377 3378 elif idInStopOrders: 3379 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3380 3381 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3382 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3383 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3384 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3385 3386 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3387 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3388 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3389 3390 else: 3391 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3392 3393 else: 3394 continue 3395 3396 def CloseAllOrders(self) -> None: 3397 """ 3398 Gets a list of open pending and stop orders and cancel it all. 3399 """ 3400 rawOrders = self.RequestPendingOrders() 3401 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3402 lenOrders = len(allOrdersIDs) 3403 3404 rawStopOrders = self.RequestStopOrders() 3405 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3406 lenSOrders = len(allStopOrdersIDs) 3407 3408 if lenOrders > 0 or lenSOrders > 0: 3409 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3410 3411 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3412 3413 else: 3414 uLogger.info("Orders not found, nothing to cancel.") 3415 3416 def CloseAll(self, *args) -> None: 3417 """ 3418 Close all available (not blocked) opened trades and orders. 3419 3420 Also, you can select one or more keywords case-insensitive: 3421 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3422 3423 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3424 """ 3425 overview = self.Overview(show=False) # get all open trades info 3426 3427 if len(args) == 0: 3428 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3429 self.CloseAllOrders() # close all pending and stop orders 3430 3431 for iType in TKS_INSTRUMENTS: 3432 if iType != "Currencies": 3433 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3434 3435 else: 3436 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3437 lowerArgs = [x.lower() for x in args] 3438 3439 if "orders" in lowerArgs: 3440 self.CloseAllOrders() # close all pending and stop orders 3441 3442 for iType in TKS_INSTRUMENTS: 3443 if iType.lower() in lowerArgs and iType != "Currencies": 3444 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3445 3446 @staticmethod 3447 def ParseOrderParameters(operation, **inputParameters): 3448 """ 3449 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3450 3451 :param operation: string "Buy" or "Sell". 3452 :param inputParameters: this is dict of strings that looks like this 3453 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3454 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3455 "prices" key: one or more prices to open limit-orders 3456 Counts of values in lots and prices lists must be equals! 3457 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3458 """ 3459 # TODO: update order grid work with api v2 3460 pass 3461 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3462 # 3463 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3464 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3465 # raise Exception("Incorrect value") 3466 # 3467 # if "l" in inputParameters.keys(): 3468 # inputParameters["lots"] = inputParameters.pop("l") 3469 # 3470 # if "p" in inputParameters.keys(): 3471 # inputParameters["prices"] = inputParameters.pop("p") 3472 # 3473 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3474 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3475 # raise Exception("Incorrect value") 3476 # 3477 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3478 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3479 # 3480 # if len(lots) != len(prices): 3481 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3482 # raise Exception("Incorrect value") 3483 # 3484 # uLogger.debug("Extracted parameters for orders:") 3485 # uLogger.debug("lots = {}".format(lots)) 3486 # uLogger.debug("prices = {}".format(prices)) 3487 # 3488 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3489 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3490 # uLogger.debug("Order parameters: {}".format(result)) 3491 # 3492 # return result 3493 3494 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3495 """ 3496 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3497 3498 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3499 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3500 """ 3501 result = False 3502 msg = "Instrument not defined!" 3503 3504 if portfolio is None or not portfolio: 3505 portfolio = self.Overview(show=False) 3506 3507 if self.ticker: 3508 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3509 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3510 3511 for iType in TKS_INSTRUMENTS: 3512 for instrument in portfolio["stat"][iType]: 3513 if instrument["ticker"] == self.ticker: 3514 result = True 3515 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3516 break 3517 3518 elif self.figi: 3519 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3520 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3521 3522 for iType in TKS_INSTRUMENTS: 3523 for instrument in portfolio["stat"][iType]: 3524 if instrument["figi"] == self.figi: 3525 result = True 3526 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3527 break 3528 3529 else: 3530 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3531 3532 uLogger.debug(msg) 3533 3534 return result 3535 3536 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3537 """ 3538 Returns instrument is in the user's portfolio if it presents there. 3539 Instrument must be defined by `ticker` (highly priority) or `figi`. 3540 3541 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3542 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3543 """ 3544 result = None 3545 msg = "Instrument not defined!" 3546 3547 if portfolio is None or not portfolio: 3548 portfolio = self.Overview(show=False) 3549 3550 if self.ticker: 3551 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3552 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3553 3554 for iType in TKS_INSTRUMENTS: 3555 for instrument in portfolio["stat"][iType]: 3556 if instrument["ticker"] == self.ticker: 3557 result = instrument 3558 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3559 break 3560 3561 elif self.figi: 3562 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3563 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3564 3565 for iType in TKS_INSTRUMENTS: 3566 for instrument in portfolio["stat"][iType]: 3567 if instrument["figi"] == self.figi: 3568 result = instrument 3569 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3570 break 3571 3572 else: 3573 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3574 3575 uLogger.debug(msg) 3576 3577 return result 3578 3579 def RequestLimits(self) -> dict: 3580 """ 3581 Method for obtaining the available funds for withdrawal for current `accountId`. 3582 3583 See also: 3584 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3585 - `OverviewLimits()` method 3586 3587 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3588 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3589 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3590 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3591 """ 3592 if self.accountId is None or not self.accountId: 3593 uLogger.error("Variable `accountId` must be defined for using this method!") 3594 raise Exception("Account ID required") 3595 3596 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3597 3598 self.body = str({"accountId": self.accountId}) 3599 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3600 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3601 3602 uLogger.debug("Records about available funds for withdrawal successfully received") 3603 3604 return rawLimits 3605 3606 def OverviewLimits(self, show: bool = False) -> dict: 3607 """ 3608 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3609 3610 See also: `RequestLimits()`. 3611 3612 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3613 :return: dict with raw parsed data from server and some calculated statistics about it. 3614 """ 3615 if self.accountId is None or not self.accountId: 3616 uLogger.error("Variable `accountId` must be defined for using this method!") 3617 raise Exception("Account ID required") 3618 3619 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3620 3621 view = { 3622 "rawLimits": rawLimits, 3623 "limits": { # parsed data for every currency: 3624 "money": { # this is an array of portfolio currency positions 3625 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3626 }, 3627 "blocked": { # this is an array of blocked currency 3628 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3629 }, 3630 "blockedGuarantee": { # this is locked money under collateral for futures 3631 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3632 }, 3633 }, 3634 } 3635 3636 # --- Prepare text table with limits in human-readable format: 3637 if show: 3638 info = [ 3639 "# Withdrawal limits\n\n", 3640 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3641 "* **Account ID:** [{}]\n".format(self.accountId), 3642 ] 3643 3644 if view["limits"]["money"]: 3645 info.extend([ 3646 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3647 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3648 ]) 3649 3650 else: 3651 info.append("\nNo withdrawal limits\n") 3652 3653 for curr in view["limits"]["money"].keys(): 3654 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3655 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3656 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3657 3658 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3659 "[{}]".format(curr), 3660 "{:.2f}".format(view["limits"]["money"][curr]), 3661 "{:.2f}".format(availableMoney), 3662 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3663 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3664 ) 3665 3666 if curr == "rub": 3667 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3668 3669 else: 3670 info.append(infoStr) 3671 3672 infoText = "".join(info) 3673 3674 uLogger.info(infoText) 3675 3676 if self.withdrawalLimitsFile: 3677 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3678 fH.write(infoText) 3679 3680 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3681 3682 return view 3683 3684 def RequestAccounts(self) -> dict: 3685 """ 3686 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3687 3688 See also: 3689 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3690 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3691 - `OverviewUserInfo()` method 3692 3693 :return: dict with raw data from server that contains accounts info. Example of dict: 3694 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3695 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3696 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3697 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3698 """ 3699 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3700 3701 self.body = str({}) 3702 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3703 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3704 3705 uLogger.debug("Records about available accounts successfully received") 3706 3707 return rawAccounts 3708 3709 def RequestUserInfo(self) -> dict: 3710 """ 3711 Method for requesting common user's information. 3712 3713 See also: 3714 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3715 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3716 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3717 - `OverviewUserInfo()` method 3718 3719 :return: dict with raw data from server that contains user's information. Example of dict: 3720 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3721 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3722 """ 3723 uLogger.debug("Requesting common user's information. Wait, please...") 3724 3725 self.body = str({}) 3726 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3727 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3728 3729 uLogger.debug("Records about current user successfully received") 3730 3731 return rawUserInfo 3732 3733 def RequestMarginStatus(self, accountId: str = None) -> dict: 3734 """ 3735 Method for requesting margin calculation for defined account ID. 3736 3737 See also: 3738 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3739 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3740 - `OverviewUserInfo()` method 3741 3742 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3743 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3744 Example of responses: 3745 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3746 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3747 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3748 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3749 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3750 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3751 """ 3752 if accountId is None or not accountId: 3753 if self.accountId is None or not self.accountId: 3754 uLogger.error("Variable `accountId` must be defined for using this method!") 3755 raise Exception("Account ID required") 3756 3757 else: 3758 accountId = self.accountId # use `self.accountId` (main ID) by default 3759 3760 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3761 3762 self.body = str({"accountId": accountId}) 3763 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3764 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3765 3766 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3767 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3768 rawMargin = {} 3769 3770 else: 3771 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3772 3773 return rawMargin 3774 3775 def RequestTariffLimits(self) -> dict: 3776 """ 3777 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3778 3779 See also: 3780 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3781 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3782 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3783 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3784 - `OverviewUserInfo()` method 3785 3786 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3787 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3788 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3789 """ 3790 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3791 3792 self.body = str({}) 3793 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3794 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3795 3796 uLogger.debug("Records with limits of current tariff successfully received") 3797 3798 return rawTariffLimits 3799 3800 def RequestBondCoupons(self, iJSON: dict) -> dict: 3801 """ 3802 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3803 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3804 All dates are in UTC timezone. 3805 3806 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3807 Documentation: 3808 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3809 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3810 3811 See also: `ExtendBondsData()`. 3812 3813 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3814 If raw iJSON is not data of bond then server returns an error [400] with message: 3815 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3816 :return: dictionary with bond payment calendar. Response example 3817 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3818 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3819 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3820 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3821 """ 3822 if iJSON["figi"] is None or not iJSON["figi"]: 3823 uLogger.error("FIGI must be defined for using this method!") 3824 raise Exception("FIGI required") 3825 3826 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3827 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3828 3829 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3830 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3831 self.figi, 3832 startDate, 3833 endDate, 3834 )) 3835 3836 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3837 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3838 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3839 3840 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3841 uLogger.warning("Instrument type is not bond!") 3842 3843 else: 3844 uLogger.debug("Records about bond payment calendar successfully received") 3845 3846 return calendar 3847 3848 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3849 """ 3850 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3851 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3852 coupon yields, current yields and some statistics etc. 3853 3854 WARNING! This is too long operation if a lot of bonds requested from broker server. 3855 3856 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3857 3858 :param instruments: list of strings with tickers or FIGIs. 3859 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3860 for further used by data scientists or stock analytics. 3861 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3862 In XLSX-file and Pandas DataFrame fields mean: 3863 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3864 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3865 """ 3866 if instruments is None or not instruments: 3867 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3868 raise Exception("Ticker or FIGI required") 3869 3870 if isinstance(instruments, str): 3871 instruments = [instruments] 3872 3873 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3874 3875 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3876 3877 iCount = len(uniqueInstruments) 3878 tooLong = iCount >= 20 3879 if tooLong: 3880 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3881 3882 bonds = None 3883 for i, self.figi in enumerate(uniqueInstruments): 3884 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3885 3886 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3887 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3888 rawBond = self.SearchByFIGI(requestPrice=True) 3889 3890 # Widen raw data with UTC current time (iData["actualDateTime"]): 3891 actualDate = datetime.now(tzutc()) 3892 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3893 3894 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3895 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3896 3897 # Replace some values with human-readable: 3898 iData["nominalCurrency"] = iData["nominal"]["currency"] 3899 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3900 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3901 iData["aciCurrency"] = iData["aciValue"]["currency"] 3902 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3903 iData["issueSize"] = int(iData["issueSize"]) 3904 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3905 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3906 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3907 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3908 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3909 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3910 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3911 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3912 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3913 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3914 3915 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3916 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3917 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3918 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3919 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3920 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3921 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3922 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3923 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3924 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3925 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3926 3927 # Widen raw data with calendar data from `rawCalendar` values: 3928 calendarData = [] 3929 for item in iData["rawCalendar"]["events"]: 3930 calendarData.append({ 3931 "couponDate": item["couponDate"], 3932 "couponNumber": int(item["couponNumber"]), 3933 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3934 "payCurrency": item["payOneBond"]["currency"], 3935 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3936 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3937 "couponStartDate": item["couponStartDate"], 3938 "couponEndDate": item["couponEndDate"], 3939 "couponPeriod": item["couponPeriod"], 3940 }) 3941 3942 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3943 if "maturityDate" not in iData.keys(): 3944 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3945 3946 # Widen raw data with Coupon Rate. 3947 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3948 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3949 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3950 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3951 3952 # Widen raw data with Yield to Maturity (YTM) on current date. 3953 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3954 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3955 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3956 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3957 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3958 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3959 3960 iData["calendar"] = calendarData # adds calendar at the end 3961 3962 # Remove not used data: 3963 iData.pop("uid") 3964 iData.pop("positionUid") 3965 iData.pop("currentPrice") 3966 iData.pop("rawCalendar") 3967 3968 colNames = list(iData.keys()) 3969 if bonds is None: 3970 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3971 3972 else: 3973 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3974 3975 else: 3976 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3977 3978 processed = round(100 * (i + 1) / iCount, 1) 3979 if tooLong and processed % 5 == 0: 3980 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3981 3982 else: 3983 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3984 3985 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3986 3987 # Saving bonds from Pandas DataFrame to XLSX sheet: 3988 if xlsx and self.bondsXLSXFile: 3989 with pd.ExcelWriter( 3990 path=self.bondsXLSXFile, 3991 date_format=TKS_DATE_FORMAT, 3992 datetime_format=TKS_DATE_TIME_FORMAT, 3993 mode="w", 3994 ) as writer: 3995 bonds.to_excel( 3996 writer, 3997 sheet_name="Extended bonds data", 3998 index=True, 3999 encoding="UTF-8", 4000 freeze_panes=(1, 1), 4001 ) # saving as XLSX-file with freeze first row and column as headers 4002 4003 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4004 4005 return bonds 4006 4007 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4008 """ 4009 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4010 4011 WARNING! This is too long operation if a lot of bonds requested from broker server. 4012 4013 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4014 4015 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4016 extended information about bonds: main info, current prices, bond payment calendar, 4017 coupon yields, current yields and some statistics etc. 4018 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4019 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4020 for further used by data scientists or stock analytics. 4021 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4022 """ 4023 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4024 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4025 4026 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4027 4028 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4029 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4030 calendar = None 4031 for bond in extBonds.iterrows(): 4032 for item in bond[1]["calendar"]: 4033 cData = { 4034 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4035 "couponDate": item["couponDate"], 4036 "figi": bond[1]["figi"], 4037 "ticker": bond[1]["ticker"], 4038 "name": bond[1]["name"], 4039 "couponNumber": item["couponNumber"], 4040 "payOneBond": item["payOneBond"], 4041 "payCurrency": item["payCurrency"], 4042 "couponType": item["couponType"], 4043 "couponPeriod": item["couponPeriod"], 4044 "fixDate": item["fixDate"], 4045 "couponStartDate": item["couponStartDate"], 4046 "couponEndDate": item["couponEndDate"], 4047 } 4048 4049 if calendar is None: 4050 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4051 4052 else: 4053 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4054 4055 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4056 4057 # Saving calendar from Pandas DataFrame to XLSX sheet: 4058 if xlsx: 4059 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4060 4061 with pd.ExcelWriter( 4062 path=xlsxCalendarFile, 4063 date_format=TKS_DATE_FORMAT, 4064 datetime_format=TKS_DATE_TIME_FORMAT, 4065 mode="w", 4066 ) as writer: 4067 humanReadable = calendar.copy(deep=True) 4068 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4069 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4070 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4071 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4072 humanReadable.columns = colNames # human-readable column names 4073 4074 humanReadable.to_excel( 4075 writer, 4076 sheet_name="Bond payments calendar", 4077 index=False, 4078 encoding="UTF-8", 4079 freeze_panes=(1, 2), 4080 ) # saving as XLSX-file with freeze first row and column as headers 4081 4082 del humanReadable # release df in memory 4083 4084 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4085 4086 return calendar 4087 4088 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4089 """ 4090 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4091 Also, creates Markdown file with calendar data, `calendar.md` by default. 4092 4093 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4094 4095 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4096 extended information about bonds: main info, current prices, bond payment calendar, 4097 coupon yields, current yields and some statistics etc. 4098 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4099 :param show: if `True` then also printing bonds payment calendar to the console, 4100 otherwise save to file `calendarFile` only. `False` by default. 4101 :return: multilines text in Markdown format with bonds payment calendar as a table. 4102 """ 4103 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4104 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4105 4106 infoText = "# Bond payments calendar\n\n" 4107 4108 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4109 4110 if not calendar.empty: 4111 splitLine = "| | | | | | | | | |\n" 4112 4113 info = [ 4114 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4115 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4116 ] 4117 4118 newMonth = False 4119 notOneBond = calendar["figi"].nunique() > 1 4120 for i, bond in enumerate(calendar.iterrows()): 4121 if newMonth and notOneBond: 4122 info.append(splitLine) 4123 4124 info.append( 4125 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4126 " √" if bond[1]["paid"] else " —", 4127 bond[1]["couponDate"].split("T")[0], 4128 bond[1]["figi"], 4129 bond[1]["ticker"], 4130 bond[1]["couponNumber"], 4131 "{} {}".format( 4132 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4133 bond[1]["payCurrency"], 4134 ), 4135 bond[1]["couponType"], 4136 bond[1]["couponPeriod"], 4137 bond[1]["fixDate"].split("T")[0], 4138 ) 4139 ) 4140 4141 if i < len(calendar.values) - 1: 4142 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4143 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4144 newMonth = False if curDate.month == nextDate.month else True 4145 4146 else: 4147 newMonth = False 4148 4149 infoText += "".join(info) 4150 4151 if show: 4152 uLogger.info("{}".format(infoText)) 4153 4154 if self.calendarFile is not None: 4155 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4156 fH.write(infoText) 4157 4158 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4159 4160 else: 4161 infoText += "No data\n" 4162 4163 return infoText 4164 4165 def OverviewAccounts(self, show: bool = False) -> dict: 4166 """ 4167 Method for parsing and show simple table with all available user accounts. 4168 4169 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4170 4171 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4172 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4173 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4174 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4175 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4176 "closed": "—", "access": "Full access" }, ...}}` 4177 """ 4178 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4179 4180 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4181 accounts = { 4182 item["id"]: { 4183 "type": TKS_ACCOUNT_TYPES[item["type"]], 4184 "name": item["name"], 4185 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4186 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4187 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4188 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4189 } for item in rawAccounts["accounts"] 4190 } 4191 4192 # Raw and parsed data with some fields replaced in "stat" section: 4193 view = { 4194 "rawAccounts": rawAccounts, 4195 "stat": accounts, 4196 } 4197 4198 # --- Prepare simple text table with only accounts data in human-readable format: 4199 if show: 4200 info = [ 4201 "# User accounts\n\n", 4202 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4203 "| Account ID | Type | Status | Name |\n", 4204 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4205 ] 4206 4207 for account in view["stat"].keys(): 4208 info.extend([ 4209 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4210 account, 4211 view["stat"][account]["type"], 4212 view["stat"][account]["status"], 4213 view["stat"][account]["name"], 4214 ) 4215 ]) 4216 4217 infoText = "".join(info) 4218 4219 uLogger.info(infoText) 4220 4221 if self.userAccountsFile: 4222 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4223 fH.write(infoText) 4224 4225 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4226 4227 return view 4228 4229 def OverviewUserInfo(self, show: bool = False) -> dict: 4230 """ 4231 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4232 4233 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4234 4235 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4236 :return: dict with raw parsed data from server and some calculated statistics about it. 4237 """ 4238 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4239 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4240 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4241 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4242 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4243 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4244 4245 # This is dict with parsed common user data: 4246 userInfo = { 4247 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4248 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4249 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4250 "tariff": rawUserInfo["tariff"], 4251 } 4252 4253 # This is an array of dict with parsed margin statuses for every account IDs: 4254 margins = {} 4255 for accountId in accounts.keys(): 4256 if rawMargins[accountId]: 4257 margins[accountId] = { 4258 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4259 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4260 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4261 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4262 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4263 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4264 } 4265 4266 else: 4267 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4268 4269 unary = {} # unary-connection limits 4270 for item in rawTariffLimits["unaryLimits"]: 4271 if item["limitPerMinute"] in unary.keys(): 4272 unary[item["limitPerMinute"]].extend(item["methods"]) 4273 4274 else: 4275 unary[item["limitPerMinute"]] = item["methods"] 4276 4277 stream = {} # stream-connection limits 4278 for item in rawTariffLimits["streamLimits"]: 4279 if item["limit"] in stream.keys(): 4280 stream[item["limit"]].extend(item["streams"]) 4281 4282 else: 4283 stream[item["limit"]] = item["streams"] 4284 4285 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4286 limits = { 4287 "unary": unary, 4288 "stream": stream, 4289 } 4290 4291 # Raw and parsed data as an output result: 4292 view = { 4293 "rawUserInfo": rawUserInfo, 4294 "rawAccounts": rawAccounts, 4295 "rawMargins": rawMargins, 4296 "rawTariffLimits": rawTariffLimits, 4297 "stat": { 4298 "userInfo": userInfo, 4299 "accounts": accounts, 4300 "margins": margins, 4301 "limits": limits, 4302 }, 4303 } 4304 4305 # --- Prepare text table with user information in human-readable format: 4306 if show: 4307 info = [ 4308 "# Full user information\n\n", 4309 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4310 "## Common information\n\n", 4311 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4312 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4313 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4314 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4315 "\n## User accounts\n\n", 4316 ] 4317 4318 for account in view["stat"]["accounts"].keys(): 4319 info.extend([ 4320 "### ID: [{}]\n\n".format(account), 4321 "| Parameters | Values |\n", 4322 "|----------------------|--------------------------------------------------------------|\n", 4323 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4324 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4325 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4326 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4327 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4328 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4329 ]) 4330 4331 if margins[account]: 4332 info.extend([ 4333 "| Margin status: | Enabled |\n", 4334 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4335 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4336 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4337 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4338 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4339 ]) 4340 4341 else: 4342 info.append("| Margin status: | Disabled |\n\n") 4343 4344 info.extend([ 4345 "\n## Current user tariff limits\n", 4346 "\nSee also:\n", 4347 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4348 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4349 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4350 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4351 "\n### Unary limits\n", 4352 ]) 4353 4354 if unary: 4355 for key, values in sorted(unary.items()): 4356 info.append("\n* Max requests per minute: {}\n".format(key)) 4357 4358 for value in values: 4359 info.append(" - {}\n".format(value)) 4360 4361 else: 4362 info.append("\nNot available\n") 4363 4364 info.append("\n### Stream limits\n") 4365 4366 if stream: 4367 for key, values in sorted(stream.items()): 4368 info.append("\n* Max stream connections: {}\n".format(key)) 4369 4370 for value in values: 4371 info.append(" - {}\n".format(value)) 4372 4373 else: 4374 info.append("\nNot available\n") 4375 4376 infoText = "".join(info) 4377 4378 uLogger.info(infoText) 4379 4380 if self.userInfoFile: 4381 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4382 fH.write(infoText) 4383 4384 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4385 4386 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
- debug: if
Truethen print more debug information, e.g. request and response parameters, headers etc.
Returns
response JSON (dictionary) from broker.
591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON
Search and return raw broker's information about instrument by its ticker.
ticker must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON
Search and return raw broker's information about instrument by its FIGI.
figi must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1107 `{"buy": [{"price": 1243.8, "quantity": 193}, 1108 {"price": 1244.0, "quantity": 168}, 1109 {"price": 1244.8, "quantity": 5}, 1110 {"price": 1245.0, "quantity": 61}, 1111 {"price": 1245.4, "quantity": 60}], 1112 "sell": [{"price": 1243.6, "quantity": 8}, 1113 {"price": 1242.6, "quantity": 10}, 1114 {"price": 1242.4, "quantity": 18}, 1115 {"price": 1242.2, "quantity": 50}, 1116 {"price": 1242.0, "quantity": 113}], 1117 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1118 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1119 - sell: list of dicts with Buyers prices, 1120 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1121 - quantity: volume value by current price in lots, 1122 - limitUp: current trade session limit price, maximum, 1123 - limitDown: current trade session limit price, minimum, 1124 - lastPrice: last deal price of the instrument, 1125 - closePrice: previous trade session close price of the instrument. 1126 1127 See also: `SearchByTicker()` and `SearchByFIGI()`. 1128 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1129 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1130 1131 :param show: if `True` then print DOM to log and console. 1132 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1133 If an error occurred then returns an empty record: 1134 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1135 """ 1136 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1137 1138 if self.depth < 1: 1139 uLogger.error("Depth of Market (DOM) must be >=1!") 1140 raise Exception("Incorrect value") 1141 1142 if not (self.ticker or self.figi): 1143 uLogger.error("self.ticker or self.figi variables must be defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 if self.ticker and not self.figi: 1147 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1148 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1149 1150 if not self.ticker and self.figi: 1151 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1153 1154 if not self.figi: 1155 uLogger.error("FIGI is not defined!") 1156 raise Exception("Ticker or FIGI required") 1157 1158 else: 1159 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1160 1161 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1162 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1163 self.body = str({"figi": self.figi, "depth": self.depth}) 1164 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1165 1166 if pricesResponse: 1167 # list of dicts with sellers orders: 1168 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1169 1170 # list of dicts with buyers orders: 1171 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1172 1173 # max price of instrument at this time: 1174 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1175 1176 # min price of instrument at this time: 1177 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1178 1179 # last price of deal with instrument: 1180 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1181 1182 # last close price of instrument: 1183 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1184 1185 else: 1186 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1187 uLogger.debug("Server response: {}".format(pricesResponse)) 1188 1189 if show: 1190 if prices["buy"] or prices["sell"]: 1191 info = [ 1192 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1193 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1194 self.ticker, 1195 self.figi, 1196 self.depth, 1197 ), 1198 "-" * 60, "\n", 1199 " Orders of Buyers | Orders of Sellers\n", 1200 "-" * 60, "\n", 1201 " Sell prices (volumes) | Buy prices (volumes)\n", 1202 "-" * 60, "\n", 1203 ] 1204 1205 if not prices["buy"]: 1206 info.append(" | No orders!\n") 1207 sumBuy = 0 1208 1209 else: 1210 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1211 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1212 for item in maxMinSorted: 1213 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1214 1215 if not prices["sell"]: 1216 info.append("No orders! |\n") 1217 sumSell = 0 1218 1219 else: 1220 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1221 for item in prices["sell"]: 1222 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1223 1224 info.extend([ 1225 "-" * 60, "\n", 1226 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1227 "-" * 60, "\n", 1228 ]) 1229 1230 infoText = "".join(info) 1231 1232 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1233 1234 else: 1235 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1236 1237 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1239 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1240 """ 1241 This method get and show information about all available broker instruments for current user account. 1242 If `instrumentsFile` string is not empty then also save information to this file. 1243 1244 :param show: if `True` then print results to console, if `False` - print only to file. 1245 :return: multi-lines string with all available broker instruments 1246 """ 1247 if not self.iList: 1248 self.iList = self.Listing() 1249 1250 info = [ 1251 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1252 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1253 ] 1254 1255 # add instruments count by type: 1256 for iType in self.iList.keys(): 1257 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1258 1259 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1260 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1261 1262 # generating info tables with all instruments by type: 1263 for iType in self.iList.keys(): 1264 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1265 1266 for instrument in self.iList[iType].keys(): 1267 iName = self.iList[iType][instrument]["name"] # instrument's name 1268 if len(iName) > 57: 1269 iName = "{}...".format(iName[:54]) # right trim for a long string 1270 1271 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1272 self.iList[iType][instrument]["ticker"], 1273 iName, 1274 self.iList[iType][instrument]["figi"], 1275 self.iList[iType][instrument]["currency"], 1276 self.iList[iType][instrument]["lot"], 1277 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1278 )) 1279 1280 infoText = "".join(info) 1281 1282 if show: 1283 uLogger.info(infoText) 1284 1285 if self.instrumentsFile: 1286 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1287 fH.write(infoText) 1288 1289 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1290 1291 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1293 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` - return list of result only. 1300 :return: list of dictionaries with all found instruments. 1301 """ 1302 if not self.iList: 1303 self.iList = self.Listing() 1304 1305 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1306 compiledPattern = re.compile(pattern, re.IGNORECASE) 1307 1308 for iType in self.iList: 1309 for instrument in self.iList[iType].values(): 1310 searchResult = compiledPattern.search(" ".join( 1311 [instrument["ticker"], instrument["figi"], instrument["name"]] 1312 )) 1313 1314 if searchResult: 1315 searchResults[iType][instrument["ticker"]] = instrument 1316 1317 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1318 info = [ 1319 "# Search results\n\n", 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile: 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1372 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1373 """ 1374 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1375 1376 :param instruments: list of strings with tickers or FIGIs. 1377 :return: list with unique instrument FIGIs only. 1378 """ 1379 requestedInstruments = [] 1380 for iName in instruments: 1381 if iName not in self.aliases.keys(): 1382 if iName not in requestedInstruments: 1383 requestedInstruments.append(iName) 1384 1385 else: 1386 if iName not in requestedInstruments: 1387 if self.aliases[iName] not in requestedInstruments: 1388 requestedInstruments.append(self.aliases[iName]) 1389 1390 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1391 1392 onlyUniqueFIGIs = [] 1393 for iName in requestedInstruments: 1394 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1395 continue 1396 1397 self.ticker = iName 1398 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1399 1400 if not iData: 1401 self.ticker = "" 1402 self.figi = iName 1403 1404 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1405 1406 if not iData: 1407 self.figi = "" 1408 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1409 1410 if iData and iData["figi"] not in onlyUniqueFIGIs: 1411 onlyUniqueFIGIs.append(iData["figi"]) 1412 1413 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1414 1415 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1417 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1418 """ 1419 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 If `pricesFile` string is not empty then also save information to this file. 1422 1423 :param instruments: list of strings with tickers or FIGIs. 1424 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1425 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1427 """ 1428 if instruments is None or not instruments: 1429 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1430 raise Exception("Ticker or FIGI required") 1431 1432 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1433 1434 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1435 1436 iList = [] # trying to get info and current prices about all unique instruments: 1437 for self.figi in onlyUniqueFIGIs: 1438 iData = self.SearchByFIGI(requestPrice=True) 1439 iList.append(iData) 1440 1441 self.ShowListOfPrices(iList, show) 1442 1443 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1445 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1446 """ 1447 Show table contains current prices of given instruments. 1448 1449 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1451 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1452 :return: multilines text in Markdown format as a table contains current prices. 1453 """ 1454 infoText = "" 1455 1456 if show or self.pricesFile: 1457 info = [ 1458 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1459 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1460 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1461 ] 1462 1463 for item in iList: 1464 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1465 item["ticker"], 1466 item["figi"], 1467 item["type"], 1468 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1469 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1470 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1471 "{} / {}".format( 1472 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1473 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1474 ), 1475 "{} / {}".format( 1476 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1477 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1478 ), 1479 item["currency"], 1480 )) 1481 1482 infoText = "".join(info) 1483 1484 if show: 1485 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1486 1487 if self.pricesFile: 1488 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1489 fH.write(infoText) 1490 1491 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1492 1493 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1495 def RequestTradingStatus(self) -> dict: 1496 """ 1497 Requesting trading status for the instrument defined by `figi` variable. 1498 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1499 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1500 1501 :return: dictionary with trading status attributes. Response example: 1502 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1503 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1504 """ 1505 if self.figi is None or not self.figi: 1506 uLogger.error("Variable `figi` must be defined for using this method!") 1507 raise Exception("FIGI required") 1508 1509 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1510 1511 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1512 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1513 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1514 1515 uLogger.debug("Records about current trading status successfully received") 1516 1517 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1519 def RequestPortfolio(self) -> dict: 1520 """ 1521 Requesting actual user's portfolio for current `accountId`. 1522 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1524 1525 :return: dictionary with user's portfolio. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1535 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1536 1537 uLogger.debug("Records about user's portfolio successfully received") 1538 1539 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1541 def RequestPositions(self) -> dict: 1542 """ 1543 Requesting open positions by currencies and instruments for current `accountId`. 1544 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1545 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1546 1547 :return: dictionary with open positions by instruments. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1557 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1558 1559 uLogger.debug("Records about current open positions successfully received") 1560 1561 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1563 def RequestPendingOrders(self) -> list: 1564 """ 1565 Requesting current actual pending orders for current `accountId`. 1566 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1580 1581 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1582 1583 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1602 1603 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1604 1605 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1607 def Overview(self, show: bool = False, details: str = "full") -> dict: 1608 """ 1609 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1610 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1611 are defined then also save information to file. 1612 1613 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1614 many requests about the state of the portfolio, and then, based on the received data, a large number 1615 of calculation and statistics are collected. 1616 1617 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1618 :param details: how detailed should the information be? You should specify one of strings: 1619 `full` - shows full available information about portfolio status (by default), 1620 `positions` - shows only open positions, 1621 `digest` - show a short digest of the portfolio status, 1622 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1623 `orders` - shows only sections of open limits and stop orders. 1624 :return: dictionary with client's raw portfolio and some statistics. 1625 """ 1626 if self.accountId is None or not self.accountId: 1627 uLogger.error("Variable `accountId` must be defined for using this method!") 1628 raise Exception("Account ID required") 1629 1630 view = { 1631 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1632 "headers": {}, # list of dictionaries, response headers without "positions" section 1633 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1634 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1635 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1636 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1637 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1638 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1639 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1640 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1641 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1642 }, 1643 "stat": { # --- some statistics calculated using "raw" sections: 1644 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1645 "availableRUB": 0., # available rubles (without other currencies) 1646 "blockedRUB": 0., # blocked sum in Russian Rouble 1647 "totalChangesRUB": 0., # changes for all open trades in RUB 1648 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1649 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1650 "sharesCostRUB": 0., # costs of all shares in RUB 1651 "bondsCostRUB": 0., # costs of all bonds in RUB 1652 "etfsCostRUB": 0., # costs of all etfs in RUB 1653 "futuresCostRUB": 0., # costs of all futures in RUB 1654 "Currencies": [], # list of dictionaries of all currencies statistics 1655 "Shares": [], # list of dictionaries of all shares statistics 1656 "Bonds": [], # list of dictionaries of all bonds statistics 1657 "Etfs": [], # list of dictionaries of all etfs statistics 1658 "Futures": [], # list of dictionaries of all futures statistics 1659 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1660 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1661 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1662 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1663 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1664 }, 1665 "analytics": { # --- some analytics of portfolio: 1666 "distrByAssets": {}, # portfolio distribution by assets 1667 "distrByCompanies": {}, # portfolio distribution by companies 1668 "distrBySectors": {}, # portfolio distribution by sectors 1669 "distrByCurrencies": {}, # portfolio distribution by currencies 1670 "distrByCountries": {}, # portfolio distribution by countries 1671 } 1672 } 1673 1674 details = details.lower() 1675 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1676 if details not in availableDetails: 1677 details = "full" 1678 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1679 1680 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1681 1682 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1683 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1684 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1685 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1686 1687 # save response headers without "positions" section: 1688 for key in portfolioResponse.keys(): 1689 if key != "positions": 1690 view["raw"]["headers"][key] = portfolioResponse[key] 1691 1692 else: 1693 continue 1694 1695 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1696 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1697 for item in portfolioResponse["positions"]: 1698 if item["instrumentType"] == "currency": 1699 self.figi = item["figi"] 1700 curr = self.SearchByFIGI(requestPrice=False) 1701 1702 # current price of currency in RUB: 1703 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1704 "name": curr["name"], 1705 "currentPrice": NanoToFloat( 1706 item["currentPrice"]["units"], 1707 item["currentPrice"]["nano"] 1708 ), 1709 } 1710 1711 view["raw"]["Currencies"].append(item) 1712 1713 elif item["instrumentType"] == "share": 1714 view["raw"]["Shares"].append(item) 1715 1716 elif item["instrumentType"] == "bond": 1717 view["raw"]["Bonds"].append(item) 1718 1719 elif item["instrumentType"] == "etf": 1720 view["raw"]["Etfs"].append(item) 1721 1722 elif item["instrumentType"] == "futures": 1723 view["raw"]["Futures"].append(item) 1724 1725 else: 1726 continue 1727 1728 # how many volume of currencies (by ISO currency name) are blocked: 1729 for item in view["raw"]["positions"]["blocked"]: 1730 blocked = NanoToFloat(item["units"], item["nano"]) 1731 if blocked > 0: 1732 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1733 1734 # how many volume of instruments (by FIGI) are blocked: 1735 for item in view["raw"]["positions"]["securities"]: 1736 blocked = int(item["blocked"]) 1737 if blocked > 0: 1738 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1739 1740 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1741 1742 if "rub" in allBlocked.keys(): 1743 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1744 1745 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1746 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1747 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1748 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1749 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1750 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1751 view["stat"]["portfolioCostRUB"] = sum([ 1752 view["stat"]["allCurrenciesCostRUB"], 1753 view["stat"]["sharesCostRUB"], 1754 view["stat"]["bondsCostRUB"], 1755 view["stat"]["etfsCostRUB"], 1756 view["stat"]["futuresCostRUB"], 1757 ]) 1758 1759 # --- calculating some portfolio statistics: 1760 byComp = {} # distribution by companies 1761 bySect = {} # distribution by sectors 1762 byCurr = {} # distribution by currencies (include RUB) 1763 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1764 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1765 1766 for item in portfolioResponse["positions"]: 1767 self.figi = item["figi"] 1768 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1769 1770 if instrument: 1771 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1772 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1773 1774 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1775 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1776 1777 else: 1778 blocked = 0 1779 1780 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1781 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1782 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1783 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1784 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1785 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1786 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1787 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1788 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1789 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1790 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1791 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1792 1793 statData = { 1794 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1795 "ticker": instrument["ticker"], # ticker by FIGI 1796 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1797 "volume": volume, # available volume of instrument 1798 "lots": lots, # volume in lots of instrument 1799 "direction": direction, # direction of an instrument's position: short or long 1800 "blocked": blocked, # blocked volume of currency or instrument 1801 "currentPrice": curPrice, # current instrument's price in basic asset 1802 "average": average, # current average position price 1803 "cost": cost, # current cost of all volume of instrument in basic asset 1804 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1805 "costRUB": costRUB, # cost of instrument in ruble 1806 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1807 "profit": profit, # expected profit at current moment 1808 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1809 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1810 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1811 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1812 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1813 "step": instrument["step"], # minimum price increment 1814 } 1815 1816 # adding distribution by unique countries: 1817 if statData["country"] not in byCountry.keys(): 1818 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1819 1820 else: 1821 byCountry[statData["country"]]["cost"] += costRUB 1822 byCountry[statData["country"]]["percent"] += percentCostRUB 1823 1824 if item["instrumentType"] != "currency": 1825 # adding distribution by unique companies: 1826 if statData["name"]: 1827 if statData["name"] not in byComp.keys(): 1828 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 byComp[statData["name"]]["cost"] += costRUB 1832 byComp[statData["name"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique sectors: 1835 if statData["sector"] not in bySect.keys(): 1836 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 bySect[statData["sector"]]["cost"] += costRUB 1840 bySect[statData["sector"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique currencies: 1843 if currency not in byCurr.keys(): 1844 byCurr[currency] = { 1845 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1846 "cost": costRUB, 1847 "percent": percentCostRUB 1848 } 1849 1850 else: 1851 byCurr[currency]["cost"] += costRUB 1852 byCurr[currency]["percent"] += percentCostRUB 1853 1854 # saving statistics for every instrument: 1855 if item["instrumentType"] == "currency": 1856 view["stat"]["Currencies"].append(statData) 1857 1858 # update dict with free funds for trading (total - blocked) by currencies 1859 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1860 view["stat"]["funds"][currency] = { 1861 "total": volume, 1862 "totalCostRUB": costRUB, # total volume cost in rubles 1863 "free": volume - blocked, 1864 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1865 } 1866 1867 elif item["instrumentType"] == "share": 1868 view["stat"]["Shares"].append(statData) 1869 1870 elif item["instrumentType"] == "bond": 1871 view["stat"]["Bonds"].append(statData) 1872 1873 elif item["instrumentType"] == "etf": 1874 view["stat"]["Etfs"].append(statData) 1875 1876 elif item["instrumentType"] == "Futures": 1877 view["stat"]["Futures"].append(statData) 1878 1879 else: 1880 continue 1881 1882 # total changes in Russian Ruble: 1883 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1884 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1885 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1886 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1887 view["stat"]["funds"]["rub"] = { 1888 "total": view["stat"]["availableRUB"], 1889 "totalCostRUB": view["stat"]["availableRUB"], 1890 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1892 } 1893 1894 # --- pending orders sector data: 1895 uniquePendingOrders = [] 1896 uniquePendingOrdersFIGIs = [] 1897 for item in view["raw"]["orders"]: 1898 if item["figi"] not in uniquePendingOrdersFIGIs: 1899 uniquePendingOrdersFIGIs.append(item["figi"]) 1900 uniquePendingOrders.append(item) 1901 1902 for item in uniquePendingOrders: 1903 self.figi = item["figi"] 1904 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrders = [] 1944 uniqueStopOrdersFIGIs = [] 1945 for item in view["raw"]["stopOrders"]: 1946 if item["figi"] not in uniqueStopOrdersFIGIs: 1947 uniqueStopOrdersFIGIs.append(item["figi"]) 1948 uniqueStopOrders.append(item) 1949 1950 for item in uniqueStopOrders: 1951 self.figi = item["figi"] 1952 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2055 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2056 2057 view["analytics"]["distrByCurrencies"].update(byCurr) 2058 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2059 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2060 2061 # portfolio distribution by countries: 2062 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2063 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2065 2066 view["analytics"]["distrByCountries"].update(byCountry) 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2068 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2069 2070 # --- Prepare text statistics overview in human-readable: 2071 if show: 2072 # Whatever the value `details`, header not changes: 2073 info = [ 2074 "# Client's portfolio\n\n", 2075 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2076 "* **Account ID:** [{}]\n".format(self.accountId), 2077 ] 2078 2079 if details in ["full", "positions", "digest"]: 2080 info.extend([ 2081 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2082 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2083 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2084 view["stat"]["totalChangesRUB"], 2085 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2086 view["stat"]["totalChangesPercentRUB"], 2087 ), 2088 ]) 2089 2090 if details in ["full", "positions"]: 2091 info.extend([ 2092 "## Open positions\n\n", 2093 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2094 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2095 "| Ruble | {:>31} | | | | | |\n".format( 2096 "{:.2f} ({:.2f}) rub".format( 2097 view["stat"]["availableRUB"], 2098 view["stat"]["blockedRUB"], 2099 ) 2100 ) 2101 ]) 2102 2103 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2104 return [ 2105 "| | | | | | | |\n", 2106 "| {:<27} | | | | | {:>19} | |\n".format( 2107 noTradeStr if noTradeStr else typeStr, 2108 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2109 ), 2110 ] 2111 2112 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2113 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2114 "{} [{}]".format(data["ticker"], data["figi"]), 2115 "{:.2f} ({:.2f}) {}".format( 2116 data["volume"], 2117 data["blocked"], 2118 data["currency"], 2119 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2120 data["volume"], 2121 data["blocked"], 2122 ), 2123 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2124 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2126 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2127 "{}{:.2f} {} ({}{:.2f}%)".format( 2128 "+" if data["profit"] > 0 else "", 2129 data["profit"], data["baseCurrencyName"], 2130 "+" if data["percentProfit"] > 0 else "", 2131 data["percentProfit"], 2132 ), 2133 ) 2134 2135 # --- Show currencies section: 2136 if view["stat"]["Currencies"]: 2137 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2138 for item in view["stat"]["Currencies"]: 2139 info.append(_InfoStr(item, showCurrencyName=True)) 2140 2141 else: 2142 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2143 2144 # --- Show shares section: 2145 if view["stat"]["Shares"]: 2146 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2147 2148 for item in view["stat"]["Shares"]: 2149 info.append(_InfoStr(item)) 2150 2151 else: 2152 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2153 2154 # --- Show bonds section: 2155 if view["stat"]["Bonds"]: 2156 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2157 2158 for item in view["stat"]["Bonds"]: 2159 info.append(_InfoStr(item)) 2160 2161 else: 2162 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2163 2164 # --- Show etfs section: 2165 if view["stat"]["Etfs"]: 2166 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2167 2168 for item in view["stat"]["Etfs"]: 2169 info.append(_InfoStr(item)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2173 2174 # --- Show futures section: 2175 if view["stat"]["Futures"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2177 2178 for item in view["stat"]["Futures"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2183 2184 if details in ["full", "orders"]: 2185 # --- Show pending orders section: 2186 if view["stat"]["orders"]: 2187 info.extend([ 2188 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2189 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2190 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2191 ]) 2192 2193 for item in view["stat"]["orders"]: 2194 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2195 "{} [{}]".format(item["ticker"], item["figi"]), 2196 item["orderID"], 2197 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2198 "{} {} ({}{:.2f}%)".format( 2199 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2200 item["baseCurrencyName"], 2201 "+" if item["percentChanges"] > 0 else "", 2202 float(item["percentChanges"]), 2203 ), 2204 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2205 item["action"], 2206 item["type"], 2207 item["date"], 2208 )) 2209 2210 else: 2211 info.append("\n## Total pending limit-orders: 0\n") 2212 2213 # --- Show stop orders section: 2214 if view["stat"]["stopOrders"]: 2215 info.extend([ 2216 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2217 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2218 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["stopOrders"]: 2222 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 item["lotsRequested"], 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2234 item["action"], 2235 item["type"], 2236 item["expType"], 2237 item["createDate"], 2238 item["expDate"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total stop-orders: 0\n") 2243 2244 if details in ["full", "analytics"]: 2245 # -- Show analytics section: 2246 if view["stat"]["portfolioCostRUB"] > 0: 2247 info.extend([ 2248 "\n# Analytics\n" 2249 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2250 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2251 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2252 view["stat"]["totalChangesRUB"], 2253 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2254 view["stat"]["totalChangesPercentRUB"], 2255 ), 2256 "\n## Portfolio distribution by assets\n" 2257 "\n| Type | Uniques | Percent | Current cost |\n", 2258 "|------------|---------|---------|--------------------|\n", 2259 ]) 2260 2261 for key in view["analytics"]["distrByAssets"].keys(): 2262 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2263 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2264 key, 2265 view["analytics"]["distrByAssets"][key]["uniques"], 2266 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2267 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2268 )) 2269 2270 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2271 info.extend([ 2272 "\n## Portfolio distribution by companies\n" 2273 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2274 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2275 ]) 2276 2277 for company in view["analytics"]["distrByCompanies"].keys(): 2278 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2279 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2280 info.append("| {} | {:<7} | {:<18} |\n".format( 2281 "{}{}{}".format( 2282 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2283 company, 2284 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2285 ), 2286 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2288 )) 2289 2290 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2291 info.extend([ 2292 "\n## Portfolio distribution by sectors\n" 2293 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2294 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2295 ]) 2296 2297 for sector in view["analytics"]["distrBySectors"].keys(): 2298 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2299 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2300 sector, 2301 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2302 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2304 )) 2305 2306 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2307 info.extend([ 2308 "\n## Portfolio distribution by currencies\n" 2309 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2310 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2311 ]) 2312 2313 for curr in view["analytics"]["distrByCurrencies"].keys(): 2314 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2315 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2316 info.append("| {} | {:<7} | {:<18} |\n".format( 2317 "[{}] {}{}".format( 2318 curr, 2319 view["analytics"]["distrByCurrencies"][curr]["name"], 2320 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2324 )) 2325 2326 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2327 info.extend([ 2328 "\n## Portfolio distribution by countries\n" 2329 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2330 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2331 ]) 2332 2333 for country in view["analytics"]["distrByCountries"].keys(): 2334 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2335 nameLen = len(country) 2336 info.append("| {} | {:<7} | {:<18} |\n".format( 2337 "{}{}".format( 2338 country, 2339 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2340 ), 2341 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2343 )) 2344 2345 infoText = "".join(info) 2346 2347 uLogger.info(infoText) 2348 2349 if details == "full" and self.overviewFile: 2350 filename = self.overviewFile 2351 2352 elif details == "digest" and self.overviewDigestFile: 2353 filename = self.overviewDigestFile 2354 2355 elif details == "positions" and self.overviewPositionsFile: 2356 filename = self.overviewPositionsFile 2357 2358 elif details == "orders" and self.overviewOrdersFile: 2359 filename = self.overviewOrdersFile 2360 2361 elif details == "analytics" and self.overviewAnalyticsFile: 2362 filename = self.overviewAnalyticsFile 2363 2364 else: 2365 filename = "" 2366 2367 if filename: 2368 with open(filename, "w", encoding="UTF-8") as fH: 2369 fH.write(infoText) 2370 2371 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2372 2373 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2375 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2376 """ 2377 Returns history operations between two given dates for current `accountId`. 2378 If `reportFile` string is not empty then also save human-readable report. 2379 Shows some statistical data of closed positions. 2380 2381 :param start: see docstring in `GetDatesAsString()` method 2382 :param end: see docstring in `GetDatesAsString()` method 2383 :param show: if `True` then also prints all records to the console. 2384 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2385 :return: original list of dictionaries with history of deals records from API ("operations" key): 2386 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2387 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2388 """ 2389 if self.accountId is None or not self.accountId: 2390 uLogger.error("Variable `accountId` must be defined for using this method!") 2391 raise Exception("Account ID required") 2392 2393 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2394 2395 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2396 2397 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2398 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2399 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2400 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2401 customStat = {} # custom statistics in additional to responseJSON 2402 2403 # --- output report in human-readable format: 2404 if show or self.reportFile: 2405 splitLine1 = "| | | | | |\n" # Summary section 2406 splitLine2 = "| | | | | | | | |\n" # Operations section 2407 nextDay = "" 2408 2409 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2410 2411 if len(ops) > 0: 2412 customStat = { 2413 "opsCount": 0, # total operations count 2414 "buyCount": 0, # buy operations 2415 "sellCount": 0, # sell operations 2416 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2417 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2418 "payIn": {"rub": 0.}, # Deposit brokerage account 2419 "payOut": {"rub": 0.}, # Withdrawals 2420 "divs": {"rub": 0.}, # Dividends income 2421 "coupons": {"rub": 0.}, # Coupon's income 2422 "brokerCom": {"rub": 0.}, # Service commissions 2423 "serviceCom": {"rub": 0.}, # Service commissions 2424 "marginCom": {"rub": 0.}, # Margin commissions 2425 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2426 } 2427 2428 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2429 for item in ops: 2430 if item["state"] == "OPERATION_STATE_EXECUTED": 2431 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2432 2433 # count buy operations: 2434 if "_BUY" in item["operationType"]: 2435 customStat["buyCount"] += 1 2436 2437 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2438 customStat["buyTotal"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["buyTotal"][item["payment"]["currency"]] = payment 2442 2443 # count sell operations: 2444 elif "_SELL" in item["operationType"]: 2445 customStat["sellCount"] += 1 2446 2447 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2448 customStat["sellTotal"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["sellTotal"][item["payment"]["currency"]] = payment 2452 2453 # count incoming operations: 2454 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2455 if item["payment"]["currency"] in customStat["payIn"].keys(): 2456 customStat["payIn"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["payIn"][item["payment"]["currency"]] = payment 2460 2461 # count withdrawals operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2463 if item["payment"]["currency"] in customStat["payOut"].keys(): 2464 customStat["payOut"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payOut"][item["payment"]["currency"]] = payment 2468 2469 # count dividends income: 2470 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2471 if item["payment"]["currency"] in customStat["divs"].keys(): 2472 customStat["divs"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["divs"][item["payment"]["currency"]] = payment 2476 2477 # count coupon's income: 2478 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2479 if item["payment"]["currency"] in customStat["coupons"].keys(): 2480 customStat["coupons"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["coupons"][item["payment"]["currency"]] = payment 2484 2485 # count broker commissions: 2486 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2487 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2488 customStat["brokerCom"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["brokerCom"][item["payment"]["currency"]] = payment 2492 2493 # count service commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2495 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2496 customStat["serviceCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["serviceCom"][item["payment"]["currency"]] = payment 2500 2501 # count margin commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2503 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2504 customStat["marginCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["marginCom"][item["payment"]["currency"]] = payment 2508 2509 # count withholding taxes: 2510 elif "_TAX" in item["operationType"]: 2511 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2512 customStat["allTaxes"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["allTaxes"][item["payment"]["currency"]] = payment 2516 2517 else: 2518 continue 2519 2520 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2521 2522 # --- view "Actions" lines: 2523 info.extend([ 2524 "| 1 | 2 | 3 | 4 | 5 |\n", 2525 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2526 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2527 "| | Buy: {:<22} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2530 ), 2531 "| | Sell: {:<21} | {:<28} | | |\n".format( 2532 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2533 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2534 ), 2535 ]) 2536 2537 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2538 for key in opsKeys: 2539 if key == "rub": 2540 continue 2541 2542 info.extend([ 2543 "| | | {:<28} | | |\n".format( 2544 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2545 ), 2546 "| | | {:<28} | | |\n".format( 2547 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2548 ), 2549 ]) 2550 2551 info.append(splitLine1) 2552 2553 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2554 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2555 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2558 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2559 ) 2560 2561 # --- view "Payments" lines: 2562 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2563 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2564 2565 for key in paymentsKeys: 2566 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2567 2568 info.append(splitLine1) 2569 2570 # --- view "Commissions and taxes" lines: 2571 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2572 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2573 2574 for key in comKeys: 2575 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2576 2577 info.append(splitLine1) 2578 2579 info.extend([ 2580 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2581 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2582 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2583 ]) 2584 2585 else: 2586 info.append("Broker returned no operations during this period\n") 2587 2588 # --- view "Operations" section: 2589 for item in ops: 2590 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2591 continue 2592 2593 else: 2594 self.figi = item["figi"] if item["figi"] else "" 2595 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2596 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2597 2598 # group of deals during one day: 2599 if nextDay and item["date"].split("T")[0] != nextDay: 2600 info.append(splitLine2) 2601 nextDay = "" 2602 2603 else: 2604 nextDay = item["date"].split("T")[0] # saving current day for splitting 2605 2606 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2607 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2608 self.figi if self.figi else "—", 2609 instrument["ticker"] if instrument else "—", 2610 instrument["type"] if instrument else "—", 2611 item["quantity"] if int(item["quantity"]) > 0 else "—", 2612 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2613 TKS_OPERATION_STATES[item["state"]], 2614 TKS_OPERATION_TYPES[item["operationType"]], 2615 )) 2616 2617 infoText = "".join(info) 2618 2619 if show: 2620 uLogger.info(infoText) 2621 2622 if self.reportFile: 2623 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2624 fH.write(infoText) 2625 2626 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2627 2628 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2630 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2631 """ 2632 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2633 2634 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2635 Warning! Broker server used ISO UTC time by default. 2636 2637 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2638 Also, `historyFile` used to update history with `onlyMissing` parameter. 2639 2640 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2641 2642 :param start: see docstring in `GetDatesAsString()` method. 2643 :param end: see docstring in `GetDatesAsString()` method. 2644 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2645 `"hour"`, `"day"`. Default: `"hour"`. 2646 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2647 False by default. Warning! History appends only from last candle to current time 2648 with always update last candle! 2649 :param csvSep: separator if csv-file is used, `,` by default. 2650 :param show: if `True` then also prints Pandas DataFrame to the console. 2651 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2652 `["date", "time", "open", "high", "low", "close", "volume"]`. 2653 """ 2654 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2655 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2656 history = None # empty pandas object for history 2657 2658 if interval not in TKS_CANDLE_INTERVALS.keys(): 2659 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2660 raise Exception("Incorrect value") 2661 2662 if not (self.ticker or self.figi): 2663 uLogger.error("Ticker or FIGI must be defined!") 2664 raise Exception("Ticker or FIGI required") 2665 2666 if self.ticker and not self.figi: 2667 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2668 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2669 2670 if self.figi and not self.ticker: 2671 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2672 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2673 2674 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2675 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2676 if interval.lower() != "day": 2677 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2678 2679 delta = dtEnd - dtStart # current UTC time minus last time in file 2680 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2681 2682 # calculate history length in candles: 2683 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2684 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2685 length += 1 # to avoid fraction time 2686 2687 # calculate data blocks count: 2688 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2689 2690 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2691 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2692 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2693 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2694 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2695 2696 tempOld = None # pandas object for old history, if --only-missing key present 2697 lastTime = None # datetime object of last old candle in file 2698 2699 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2700 uLogger.debug("--only-missing key present, add only last missing candles...") 2701 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2702 2703 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2704 2705 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2706 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2707 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2708 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2709 2710 # get last datetime object from last string in file or minus 1 delta if file is empty: 2711 if len(tempOld) > 0: 2712 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2713 2714 else: 2715 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2716 2717 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2718 2719 responseJSONs = [] # raw history blocks of data 2720 2721 blockEnd = dtEnd 2722 for item in range(blocks): 2723 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2724 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2725 2726 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2727 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2728 )) 2729 2730 if blockStart == blockEnd: 2731 uLogger.debug("Skipped this zero-length block...") 2732 2733 else: 2734 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2735 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2736 self.body = str({ 2737 "figi": self.figi, 2738 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2739 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2740 "interval": TKS_CANDLE_INTERVALS[interval][0] 2741 }) 2742 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2743 2744 if "code" in responseJSON.keys(): 2745 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2746 2747 else: 2748 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2749 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2750 2751 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2752 2753 blockEnd = blockStart 2754 2755 printCount = len(responseJSONs) # candles to show in console 2756 if responseJSONs: 2757 tempHistory = pd.DataFrame( 2758 data={ 2759 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2761 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2762 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2763 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2764 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2765 "volume": [int(item["volume"]) for item in responseJSONs], 2766 }, 2767 index=range(len(responseJSONs)), 2768 columns=["date", "time", "open", "high", "low", "close", "volume"], 2769 ) 2770 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2771 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2772 2773 # append only newest candles to old history if --only-missing key present: 2774 if onlyMissing and tempOld is not None and lastTime is not None: 2775 index = 0 # find start index in tempHistory data: 2776 2777 for i, item in tempHistory.iterrows(): 2778 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2779 2780 if curTime == lastTime: 2781 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2782 index = i 2783 printCount = index + 1 2784 break 2785 2786 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2787 2788 else: 2789 history = tempHistory # if no `--only-missing` key then load full data from server 2790 2791 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2792 2793 if history is not None and not history.empty: 2794 if show: 2795 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2796 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2797 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2798 )) 2799 2800 else: 2801 uLogger.warning("Received an empty candles history!") 2802 2803 if self.historyFile is not None: 2804 if history is not None and not history.empty: 2805 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2806 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2807 2808 else: 2809 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2810 2811 else: 2812 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2813 2814 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2816 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2817 """ 2818 Load candles history from csv-file and return Pandas DataFrame object. 2819 2820 See also: `History()` and `ShowHistoryChart()` methods. 2821 2822 :param filePath: path to csv-file to open. 2823 """ 2824 loadedHistory = None # init candles data object 2825 2826 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2827 2828 if os.path.exists(filePath): 2829 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2830 2831 tfStr = self.priceModel.FormattedDelta( 2832 self.priceModel.timeframe, 2833 "{days} days {hours}h {minutes}m {seconds}s", 2834 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2835 self.priceModel.timeframe, 2836 "{hours}h {minutes}m {seconds}s", 2837 ) 2838 2839 if loadedHistory is not None and not loadedHistory.empty: 2840 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2841 len(loadedHistory), 2842 tfStr, 2843 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2844 ) 2845 2846 else: 2847 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2848 2849 else: 2850 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2851 2852 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2854 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2855 """ 2856 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2857 2858 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2859 Default: `index.html` (both for interact and non-interact candlesticks chart). 2860 2861 See also: `History()` and `LoadHistory()` methods. 2862 2863 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2864 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2865 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2866 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2867 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2868 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2869 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2870 """ 2871 if isinstance(candles, str): 2872 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2873 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2874 2875 elif isinstance(candles, pd.DataFrame): 2876 self.priceModel.prices = candles # set candles chain from variable 2877 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2878 2879 if "datetime" not in candles.columns: 2880 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2881 2882 else: 2883 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2884 raise Exception("Incorrect value") 2885 2886 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2887 2888 if interact: 2889 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2890 2891 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2892 2893 else: 2894 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2895 2896 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2897 2898 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2900 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2901 """ 2902 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2903 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2904 2905 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2906 2907 :param operation: string "Buy" or "Sell". 2908 :param lots: volume, integer count of lots >= 1. 2909 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2910 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2911 :param expDate: string "Undefined" by default or local date in future, 2912 it is a string with format `%Y-%m-%d %H:%M:%S`. 2913 :return: JSON with response from broker server. 2914 """ 2915 if self.accountId is None or not self.accountId: 2916 uLogger.error("Variable `accountId` must be defined for using this method!") 2917 raise Exception("Account ID required") 2918 2919 if operation is None or not operation or operation not in ("Buy", "Sell"): 2920 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2921 raise Exception("Incorrect value") 2922 2923 if lots is None or lots < 1: 2924 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2925 lots = 1 2926 2927 if tp is None or tp < 0: 2928 tp = 0 2929 2930 if sl is None or sl < 0: 2931 sl = 0 2932 2933 if expDate is None or not expDate: 2934 expDate = "Undefined" 2935 2936 if not (self.ticker or self.figi): 2937 uLogger.error("Ticker or FIGI must be defined!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2941 self.ticker = instrument["ticker"] 2942 self.figi = instrument["figi"] 2943 2944 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2945 2946 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2947 self.body = str({ 2948 "figi": self.figi, 2949 "quantity": str(lots), 2950 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2951 "accountId": str(self.accountId), 2952 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2953 }) 2954 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2955 2956 if "orderId" in response.keys(): 2957 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2958 operation, response["orderId"], 2959 self.ticker, self.figi, lots, 2960 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2961 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2962 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2963 )) 2964 2965 else: 2966 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2967 2968 if tp > 0: 2969 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2970 2971 if sl > 0: 2972 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2973 2974 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2976 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2977 """ 2978 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2979 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2980 2981 See also: `Order()` and `Trade()` docstrings. 2982 2983 :param lots: volume, integer count of lots >= 1. 2984 :param tp: float > 0, take profit price of stop-order. 2985 :param sl: float > 0, stop loss price of stop-order. 2986 :param expDate: it's a local date in future. 2987 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2988 :return: JSON with response from broker server. 2989 """ 2990 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2992 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` and `Trade()` docstrings. 2998 2999 :param lots: volume, integer count of lots >= 1. 3000 :param tp: float > 0, take profit price of stop-order. 3001 :param sl: float > 0, stop loss price of stop-order. 3002 :param expDate: it's a local date in the future. 3003 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3004 :return: JSON with response from broker server. 3005 """ 3006 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3008 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 3009 """ 3010 Close position of given instruments. 3011 3012 :param tickers: tickers list of instruments that must be closed. 3013 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3014 This avoids unnecessary downloading data from the server. 3015 """ 3016 if not tickers: 3017 uLogger.info("Tickers list is empty, nothing to close.") 3018 3019 else: 3020 if portfolio is None or not portfolio: 3021 portfolio = self.Overview(show=False) 3022 3023 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3024 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3025 3026 for ticker in tickers: 3027 if ticker not in allOpenedTickers: 3028 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3029 continue 3030 3031 # search open trade info about instrument by ticker: 3032 instrument = {} 3033 for iType in TKS_INSTRUMENTS: 3034 if instrument: 3035 break 3036 3037 for item in portfolio["stat"][iType]: 3038 if item["ticker"] == ticker: 3039 instrument = item 3040 break 3041 3042 if instrument: 3043 self.ticker = ticker 3044 self.figi = instrument["figi"] 3045 3046 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3047 self.ticker, 3048 self.figi, 3049 int(instrument["volume"]), 3050 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3051 )) 3052 3053 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3054 3055 if tradeLots > 0: 3056 if instrument["blocked"] > 0: 3057 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3058 instrument["blocked"], 3059 self.ticker, 3060 tradeLots, 3061 )) 3062 3063 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3064 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3065 3066 else: 3067 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- tickers: tickers list of instruments that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3069 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3070 """ 3071 Close all positions of given instruments with defined type. 3072 3073 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3074 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3075 This avoids unnecessary downloading data from the server. 3076 """ 3077 if iType not in TKS_INSTRUMENTS: 3078 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3079 3080 else: 3081 if portfolio is None or not portfolio: 3082 portfolio = self.Overview(show=False) 3083 3084 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3085 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3086 3087 if tickers and portfolio: 3088 self.CloseTrades(tickers, portfolio) 3089 3090 else: 3091 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3093 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3094 """ 3095 Universal method to create market or limit orders with all available parameters for current `accountId`. 3096 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3097 3098 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3099 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3100 3101 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3102 then broker immediately open market order as you can do simple --buy or --sell operations! 3103 3104 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3105 When current price will go up or down to target price value then broker opens a limit order. 3106 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3107 3108 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3109 3110 :param operation: string "Buy" or "Sell". 3111 :param orderType: string "Limit" or "Stop". 3112 :param lots: volume, integer count of lots >= 1. 3113 :param targetPrice: target price > 0. This is open trade price for limit order. 3114 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3115 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3116 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3117 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3118 Stop loss order always executed by market price. 3119 :param expDate: string "Undefined" by default or local date in future. 3120 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3121 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3122 A limit order has no expiration date, it lasts until the end of the trading day. 3123 :return: JSON with response from broker server. 3124 """ 3125 if self.accountId is None or not self.accountId: 3126 uLogger.error("Variable `accountId` must be defined for using this method!") 3127 raise Exception("Account ID required") 3128 3129 if operation is None or not operation or operation not in ("Buy", "Sell"): 3130 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3131 raise Exception("Incorrect value") 3132 3133 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3134 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3135 raise Exception("Incorrect value") 3136 3137 if lots is None or lots < 1: 3138 uLogger.error("You must define trade volume > 0: integer count of lots!") 3139 raise Exception("Incorrect value") 3140 3141 if targetPrice is None or targetPrice <= 0: 3142 uLogger.error("Target price for limit-order must be greater than 0!") 3143 raise Exception("Incorrect value") 3144 3145 if limitPrice is None or limitPrice <= 0: 3146 limitPrice = targetPrice 3147 3148 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3149 stopType = "Limit" 3150 3151 if expDate is None or not expDate: 3152 expDate = "Undefined" 3153 3154 if not (self.ticker or self.figi): 3155 uLogger.error("Tocker or FIGI must be defined!") 3156 raise Exception("Ticker or FIGI required") 3157 3158 response = {} 3159 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3160 self.ticker = instrument["ticker"] 3161 self.figi = instrument["figi"] 3162 3163 if orderType == "Limit": 3164 uLogger.debug( 3165 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3166 self.ticker, self.figi, 3167 operation, lots, targetPrice, instrument["currency"], 3168 )) 3169 3170 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3171 self.body = str({ 3172 "figi": self.figi, 3173 "quantity": str(lots), 3174 "price": FloatToNano(targetPrice), 3175 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3176 "accountId": str(self.accountId), 3177 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3178 }) 3179 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3180 3181 if "orderId" in response.keys(): 3182 uLogger.info( 3183 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3184 response["orderId"], 3185 self.ticker, self.figi, 3186 operation, lots, targetPrice, instrument["currency"], 3187 )) 3188 3189 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3190 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3191 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3197 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3198 targetPrice, instrument["currency"], 3199 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3200 )) 3201 3202 else: 3203 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3204 3205 if orderType == "Stop": 3206 uLogger.debug( 3207 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3208 self.ticker, self.figi, 3209 operation, lots, 3210 targetPrice, instrument["currency"], 3211 limitPrice, instrument["currency"], 3212 stopType, expDate, 3213 )) 3214 3215 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3216 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3217 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3218 3219 body = { 3220 "figi": self.figi, 3221 "quantity": str(lots), 3222 "price": FloatToNano(limitPrice), 3223 "stopPrice": FloatToNano(targetPrice), 3224 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3225 "accountId": str(self.accountId), 3226 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3227 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3228 } 3229 3230 if expDateUTC: 3231 body["expireDate"] = expDateUTC 3232 3233 self.body = str(body) 3234 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3235 3236 if "stopOrderId" in response.keys(): 3237 uLogger.info( 3238 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3239 response["stopOrderId"], 3240 self.ticker, self.figi, 3241 operation, lots, 3242 targetPrice, instrument["currency"], 3243 limitPrice, instrument["currency"], 3244 TKS_STOP_ORDER_TYPES[stopOrderType], 3245 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3246 )) 3247 3248 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3249 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3250 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3256 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 else: 3262 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3263 3264 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3266 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3267 """ 3268 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3269 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3270 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3271 See also: `Order()` docstring. 3272 3273 :param lots: volume, integer count of lots >= 1. 3274 :param targetPrice: target price > 0. This is open trade price for limit order. 3275 :return: JSON with response from broker server. 3276 """ 3277 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3279 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3280 """ 3281 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3282 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3283 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3284 target price value then broker opens a limit order. See also: `Order()` docstring. 3285 3286 :param lots: volume, integer count of lots >= 1. 3287 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3288 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3289 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3290 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3291 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3292 :param expDate: string "Undefined" by default or local date in future. 3293 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3294 This date is converting to UTC format for server. 3295 :return: JSON with response from broker server. 3296 """ 3297 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3299 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3300 """ 3301 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3302 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3303 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3304 See also: `Order()` docstring. 3305 3306 :param lots: volume, integer count of lots >= 1. 3307 :param targetPrice: target price > 0. This is open trade price for limit order. 3308 :return: JSON with response from broker server. 3309 """ 3310 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3312 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3313 """ 3314 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3315 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3316 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3317 target price value then broker opens a limit order. See also: `Order()` docstring. 3318 3319 :param lots: volume, integer count of lots >= 1. 3320 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3321 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3322 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3323 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3324 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3325 :param expDate: string "Undefined" by default or local date in future. 3326 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3327 This date is converting to UTC format for server. 3328 :return: JSON with response from broker server. 3329 """ 3330 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3332 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3333 """ 3334 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3335 3336 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3337 :param allOrdersIDs: pre-received lists of all active pending orders. 3338 This avoids unnecessary downloading data from the server. 3339 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3340 """ 3341 if self.accountId is None or not self.accountId: 3342 uLogger.error("Variable `accountId` must be defined for using this method!") 3343 raise Exception("Account ID required") 3344 3345 if orderIDs: 3346 if allOrdersIDs is None or not allOrdersIDs: 3347 rawOrders = self.RequestPendingOrders() 3348 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3349 3350 if allStopOrdersIDs is None or not allStopOrdersIDs: 3351 rawStopOrders = self.RequestStopOrders() 3352 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3353 3354 for orderID in orderIDs: 3355 idInPendingOrders = orderID in allOrdersIDs 3356 idInStopOrders = orderID in allStopOrdersIDs 3357 3358 if not (idInPendingOrders or idInStopOrders): 3359 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3360 continue 3361 3362 else: 3363 if idInPendingOrders: 3364 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3365 3366 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3367 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3368 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3369 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3370 3371 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3372 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3373 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3374 3375 else: 3376 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3377 3378 elif idInStopOrders: 3379 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3380 3381 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3382 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3383 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3384 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3385 3386 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3387 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3388 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3389 3390 else: 3391 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3392 3393 else: 3394 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3396 def CloseAllOrders(self) -> None: 3397 """ 3398 Gets a list of open pending and stop orders and cancel it all. 3399 """ 3400 rawOrders = self.RequestPendingOrders() 3401 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3402 lenOrders = len(allOrdersIDs) 3403 3404 rawStopOrders = self.RequestStopOrders() 3405 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3406 lenSOrders = len(allStopOrdersIDs) 3407 3408 if lenOrders > 0 or lenSOrders > 0: 3409 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3410 3411 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3412 3413 else: 3414 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3416 def CloseAll(self, *args) -> None: 3417 """ 3418 Close all available (not blocked) opened trades and orders. 3419 3420 Also, you can select one or more keywords case-insensitive: 3421 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3422 3423 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3424 """ 3425 overview = self.Overview(show=False) # get all open trades info 3426 3427 if len(args) == 0: 3428 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3429 self.CloseAllOrders() # close all pending and stop orders 3430 3431 for iType in TKS_INSTRUMENTS: 3432 if iType != "Currencies": 3433 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3434 3435 else: 3436 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3437 lowerArgs = [x.lower() for x in args] 3438 3439 if "orders" in lowerArgs: 3440 self.CloseAllOrders() # close all pending and stop orders 3441 3442 for iType in TKS_INSTRUMENTS: 3443 if iType.lower() in lowerArgs and iType != "Currencies": 3444 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3446 @staticmethod 3447 def ParseOrderParameters(operation, **inputParameters): 3448 """ 3449 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3450 3451 :param operation: string "Buy" or "Sell". 3452 :param inputParameters: this is dict of strings that looks like this 3453 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3454 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3455 "prices" key: one or more prices to open limit-orders 3456 Counts of values in lots and prices lists must be equals! 3457 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3458 """ 3459 # TODO: update order grid work with api v2 3460 pass 3461 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3462 # 3463 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3464 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3465 # raise Exception("Incorrect value") 3466 # 3467 # if "l" in inputParameters.keys(): 3468 # inputParameters["lots"] = inputParameters.pop("l") 3469 # 3470 # if "p" in inputParameters.keys(): 3471 # inputParameters["prices"] = inputParameters.pop("p") 3472 # 3473 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3474 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3475 # raise Exception("Incorrect value") 3476 # 3477 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3478 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3479 # 3480 # if len(lots) != len(prices): 3481 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3482 # raise Exception("Incorrect value") 3483 # 3484 # uLogger.debug("Extracted parameters for orders:") 3485 # uLogger.debug("lots = {}".format(lots)) 3486 # uLogger.debug("prices = {}".format(prices)) 3487 # 3488 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3489 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3490 # uLogger.debug("Order parameters: {}".format(result)) 3491 # 3492 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3494 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3495 """ 3496 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3497 3498 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3499 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3500 """ 3501 result = False 3502 msg = "Instrument not defined!" 3503 3504 if portfolio is None or not portfolio: 3505 portfolio = self.Overview(show=False) 3506 3507 if self.ticker: 3508 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3509 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3510 3511 for iType in TKS_INSTRUMENTS: 3512 for instrument in portfolio["stat"][iType]: 3513 if instrument["ticker"] == self.ticker: 3514 result = True 3515 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3516 break 3517 3518 elif self.figi: 3519 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3520 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3521 3522 for iType in TKS_INSTRUMENTS: 3523 for instrument in portfolio["stat"][iType]: 3524 if instrument["figi"] == self.figi: 3525 result = True 3526 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3527 break 3528 3529 else: 3530 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3531 3532 uLogger.debug(msg) 3533 3534 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3536 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3537 """ 3538 Returns instrument is in the user's portfolio if it presents there. 3539 Instrument must be defined by `ticker` (highly priority) or `figi`. 3540 3541 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3542 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3543 """ 3544 result = None 3545 msg = "Instrument not defined!" 3546 3547 if portfolio is None or not portfolio: 3548 portfolio = self.Overview(show=False) 3549 3550 if self.ticker: 3551 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3552 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3553 3554 for iType in TKS_INSTRUMENTS: 3555 for instrument in portfolio["stat"][iType]: 3556 if instrument["ticker"] == self.ticker: 3557 result = instrument 3558 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3559 break 3560 3561 elif self.figi: 3562 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3563 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3564 3565 for iType in TKS_INSTRUMENTS: 3566 for instrument in portfolio["stat"][iType]: 3567 if instrument["figi"] == self.figi: 3568 result = instrument 3569 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3570 break 3571 3572 else: 3573 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3574 3575 uLogger.debug(msg) 3576 3577 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3579 def RequestLimits(self) -> dict: 3580 """ 3581 Method for obtaining the available funds for withdrawal for current `accountId`. 3582 3583 See also: 3584 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3585 - `OverviewLimits()` method 3586 3587 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3588 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3589 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3590 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3591 """ 3592 if self.accountId is None or not self.accountId: 3593 uLogger.error("Variable `accountId` must be defined for using this method!") 3594 raise Exception("Account ID required") 3595 3596 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3597 3598 self.body = str({"accountId": self.accountId}) 3599 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3600 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3601 3602 uLogger.debug("Records about available funds for withdrawal successfully received") 3603 3604 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3606 def OverviewLimits(self, show: bool = False) -> dict: 3607 """ 3608 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3609 3610 See also: `RequestLimits()`. 3611 3612 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3613 :return: dict with raw parsed data from server and some calculated statistics about it. 3614 """ 3615 if self.accountId is None or not self.accountId: 3616 uLogger.error("Variable `accountId` must be defined for using this method!") 3617 raise Exception("Account ID required") 3618 3619 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3620 3621 view = { 3622 "rawLimits": rawLimits, 3623 "limits": { # parsed data for every currency: 3624 "money": { # this is an array of portfolio currency positions 3625 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3626 }, 3627 "blocked": { # this is an array of blocked currency 3628 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3629 }, 3630 "blockedGuarantee": { # this is locked money under collateral for futures 3631 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3632 }, 3633 }, 3634 } 3635 3636 # --- Prepare text table with limits in human-readable format: 3637 if show: 3638 info = [ 3639 "# Withdrawal limits\n\n", 3640 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3641 "* **Account ID:** [{}]\n".format(self.accountId), 3642 ] 3643 3644 if view["limits"]["money"]: 3645 info.extend([ 3646 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3647 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3648 ]) 3649 3650 else: 3651 info.append("\nNo withdrawal limits\n") 3652 3653 for curr in view["limits"]["money"].keys(): 3654 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3655 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3656 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3657 3658 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3659 "[{}]".format(curr), 3660 "{:.2f}".format(view["limits"]["money"][curr]), 3661 "{:.2f}".format(availableMoney), 3662 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3663 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3664 ) 3665 3666 if curr == "rub": 3667 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3668 3669 else: 3670 info.append(infoStr) 3671 3672 infoText = "".join(info) 3673 3674 uLogger.info(infoText) 3675 3676 if self.withdrawalLimitsFile: 3677 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3678 fH.write(infoText) 3679 3680 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3681 3682 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3684 def RequestAccounts(self) -> dict: 3685 """ 3686 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3687 3688 See also: 3689 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3690 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3691 - `OverviewUserInfo()` method 3692 3693 :return: dict with raw data from server that contains accounts info. Example of dict: 3694 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3695 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3696 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3697 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3698 """ 3699 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3700 3701 self.body = str({}) 3702 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3703 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3704 3705 uLogger.debug("Records about available accounts successfully received") 3706 3707 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3709 def RequestUserInfo(self) -> dict: 3710 """ 3711 Method for requesting common user's information. 3712 3713 See also: 3714 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3715 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3716 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3717 - `OverviewUserInfo()` method 3718 3719 :return: dict with raw data from server that contains user's information. Example of dict: 3720 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3721 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3722 """ 3723 uLogger.debug("Requesting common user's information. Wait, please...") 3724 3725 self.body = str({}) 3726 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3727 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3728 3729 uLogger.debug("Records about current user successfully received") 3730 3731 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3733 def RequestMarginStatus(self, accountId: str = None) -> dict: 3734 """ 3735 Method for requesting margin calculation for defined account ID. 3736 3737 See also: 3738 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3739 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3740 - `OverviewUserInfo()` method 3741 3742 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3743 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3744 Example of responses: 3745 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3746 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3747 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3748 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3749 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3750 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3751 """ 3752 if accountId is None or not accountId: 3753 if self.accountId is None or not self.accountId: 3754 uLogger.error("Variable `accountId` must be defined for using this method!") 3755 raise Exception("Account ID required") 3756 3757 else: 3758 accountId = self.accountId # use `self.accountId` (main ID) by default 3759 3760 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3761 3762 self.body = str({"accountId": accountId}) 3763 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3764 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3765 3766 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3767 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3768 rawMargin = {} 3769 3770 else: 3771 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3772 3773 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3775 def RequestTariffLimits(self) -> dict: 3776 """ 3777 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3778 3779 See also: 3780 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3781 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3782 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3783 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3784 - `OverviewUserInfo()` method 3785 3786 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3787 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3788 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3789 """ 3790 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3791 3792 self.body = str({}) 3793 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3794 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3795 3796 uLogger.debug("Records with limits of current tariff successfully received") 3797 3798 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3800 def RequestBondCoupons(self, iJSON: dict) -> dict: 3801 """ 3802 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3803 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3804 All dates are in UTC timezone. 3805 3806 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3807 Documentation: 3808 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3809 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3810 3811 See also: `ExtendBondsData()`. 3812 3813 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3814 If raw iJSON is not data of bond then server returns an error [400] with message: 3815 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3816 :return: dictionary with bond payment calendar. Response example 3817 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3818 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3819 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3820 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3821 """ 3822 if iJSON["figi"] is None or not iJSON["figi"]: 3823 uLogger.error("FIGI must be defined for using this method!") 3824 raise Exception("FIGI required") 3825 3826 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3827 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3828 3829 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3830 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3831 self.figi, 3832 startDate, 3833 endDate, 3834 )) 3835 3836 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3837 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3838 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3839 3840 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3841 uLogger.warning("Instrument type is not bond!") 3842 3843 else: 3844 uLogger.debug("Records about bond payment calendar successfully received") 3845 3846 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3848 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3849 """ 3850 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3851 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3852 coupon yields, current yields and some statistics etc. 3853 3854 WARNING! This is too long operation if a lot of bonds requested from broker server. 3855 3856 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3857 3858 :param instruments: list of strings with tickers or FIGIs. 3859 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3860 for further used by data scientists or stock analytics. 3861 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3862 In XLSX-file and Pandas DataFrame fields mean: 3863 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3864 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3865 """ 3866 if instruments is None or not instruments: 3867 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3868 raise Exception("Ticker or FIGI required") 3869 3870 if isinstance(instruments, str): 3871 instruments = [instruments] 3872 3873 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3874 3875 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3876 3877 iCount = len(uniqueInstruments) 3878 tooLong = iCount >= 20 3879 if tooLong: 3880 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3881 3882 bonds = None 3883 for i, self.figi in enumerate(uniqueInstruments): 3884 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3885 3886 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3887 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3888 rawBond = self.SearchByFIGI(requestPrice=True) 3889 3890 # Widen raw data with UTC current time (iData["actualDateTime"]): 3891 actualDate = datetime.now(tzutc()) 3892 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3893 3894 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3895 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3896 3897 # Replace some values with human-readable: 3898 iData["nominalCurrency"] = iData["nominal"]["currency"] 3899 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3900 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3901 iData["aciCurrency"] = iData["aciValue"]["currency"] 3902 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3903 iData["issueSize"] = int(iData["issueSize"]) 3904 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3905 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3906 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3907 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3908 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3909 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3910 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3911 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3912 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3913 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3914 3915 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3916 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3917 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3918 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3919 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3920 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3921 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3922 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3923 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3924 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3925 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3926 3927 # Widen raw data with calendar data from `rawCalendar` values: 3928 calendarData = [] 3929 for item in iData["rawCalendar"]["events"]: 3930 calendarData.append({ 3931 "couponDate": item["couponDate"], 3932 "couponNumber": int(item["couponNumber"]), 3933 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3934 "payCurrency": item["payOneBond"]["currency"], 3935 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3936 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3937 "couponStartDate": item["couponStartDate"], 3938 "couponEndDate": item["couponEndDate"], 3939 "couponPeriod": item["couponPeriod"], 3940 }) 3941 3942 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3943 if "maturityDate" not in iData.keys(): 3944 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3945 3946 # Widen raw data with Coupon Rate. 3947 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3948 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3949 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3950 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3951 3952 # Widen raw data with Yield to Maturity (YTM) on current date. 3953 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3954 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3955 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3956 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3957 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3958 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3959 3960 iData["calendar"] = calendarData # adds calendar at the end 3961 3962 # Remove not used data: 3963 iData.pop("uid") 3964 iData.pop("positionUid") 3965 iData.pop("currentPrice") 3966 iData.pop("rawCalendar") 3967 3968 colNames = list(iData.keys()) 3969 if bonds is None: 3970 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3971 3972 else: 3973 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3974 3975 else: 3976 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3977 3978 processed = round(100 * (i + 1) / iCount, 1) 3979 if tooLong and processed % 5 == 0: 3980 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3981 3982 else: 3983 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3984 3985 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3986 3987 # Saving bonds from Pandas DataFrame to XLSX sheet: 3988 if xlsx and self.bondsXLSXFile: 3989 with pd.ExcelWriter( 3990 path=self.bondsXLSXFile, 3991 date_format=TKS_DATE_FORMAT, 3992 datetime_format=TKS_DATE_TIME_FORMAT, 3993 mode="w", 3994 ) as writer: 3995 bonds.to_excel( 3996 writer, 3997 sheet_name="Extended bonds data", 3998 index=True, 3999 encoding="UTF-8", 4000 freeze_panes=(1, 1), 4001 ) # saving as XLSX-file with freeze first row and column as headers 4002 4003 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4004 4005 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4007 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4008 """ 4009 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4010 4011 WARNING! This is too long operation if a lot of bonds requested from broker server. 4012 4013 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4014 4015 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4016 extended information about bonds: main info, current prices, bond payment calendar, 4017 coupon yields, current yields and some statistics etc. 4018 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4019 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4020 for further used by data scientists or stock analytics. 4021 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4022 """ 4023 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4024 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4025 4026 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4027 4028 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4029 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4030 calendar = None 4031 for bond in extBonds.iterrows(): 4032 for item in bond[1]["calendar"]: 4033 cData = { 4034 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4035 "couponDate": item["couponDate"], 4036 "figi": bond[1]["figi"], 4037 "ticker": bond[1]["ticker"], 4038 "name": bond[1]["name"], 4039 "couponNumber": item["couponNumber"], 4040 "payOneBond": item["payOneBond"], 4041 "payCurrency": item["payCurrency"], 4042 "couponType": item["couponType"], 4043 "couponPeriod": item["couponPeriod"], 4044 "fixDate": item["fixDate"], 4045 "couponStartDate": item["couponStartDate"], 4046 "couponEndDate": item["couponEndDate"], 4047 } 4048 4049 if calendar is None: 4050 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4051 4052 else: 4053 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4054 4055 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4056 4057 # Saving calendar from Pandas DataFrame to XLSX sheet: 4058 if xlsx: 4059 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4060 4061 with pd.ExcelWriter( 4062 path=xlsxCalendarFile, 4063 date_format=TKS_DATE_FORMAT, 4064 datetime_format=TKS_DATE_TIME_FORMAT, 4065 mode="w", 4066 ) as writer: 4067 humanReadable = calendar.copy(deep=True) 4068 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4069 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4070 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4071 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4072 humanReadable.columns = colNames # human-readable column names 4073 4074 humanReadable.to_excel( 4075 writer, 4076 sheet_name="Bond payments calendar", 4077 index=False, 4078 encoding="UTF-8", 4079 freeze_panes=(1, 2), 4080 ) # saving as XLSX-file with freeze first row and column as headers 4081 4082 del humanReadable # release df in memory 4083 4084 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4085 4086 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4088 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4089 """ 4090 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4091 Also, creates Markdown file with calendar data, `calendar.md` by default. 4092 4093 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4094 4095 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4096 extended information about bonds: main info, current prices, bond payment calendar, 4097 coupon yields, current yields and some statistics etc. 4098 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4099 :param show: if `True` then also printing bonds payment calendar to the console, 4100 otherwise save to file `calendarFile` only. `False` by default. 4101 :return: multilines text in Markdown format with bonds payment calendar as a table. 4102 """ 4103 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4104 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4105 4106 infoText = "# Bond payments calendar\n\n" 4107 4108 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4109 4110 if not calendar.empty: 4111 splitLine = "| | | | | | | | | |\n" 4112 4113 info = [ 4114 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4115 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4116 ] 4117 4118 newMonth = False 4119 notOneBond = calendar["figi"].nunique() > 1 4120 for i, bond in enumerate(calendar.iterrows()): 4121 if newMonth and notOneBond: 4122 info.append(splitLine) 4123 4124 info.append( 4125 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4126 " √" if bond[1]["paid"] else " —", 4127 bond[1]["couponDate"].split("T")[0], 4128 bond[1]["figi"], 4129 bond[1]["ticker"], 4130 bond[1]["couponNumber"], 4131 "{} {}".format( 4132 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4133 bond[1]["payCurrency"], 4134 ), 4135 bond[1]["couponType"], 4136 bond[1]["couponPeriod"], 4137 bond[1]["fixDate"].split("T")[0], 4138 ) 4139 ) 4140 4141 if i < len(calendar.values) - 1: 4142 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4143 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4144 newMonth = False if curDate.month == nextDate.month else True 4145 4146 else: 4147 newMonth = False 4148 4149 infoText += "".join(info) 4150 4151 if show: 4152 uLogger.info("{}".format(infoText)) 4153 4154 if self.calendarFile is not None: 4155 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4156 fH.write(infoText) 4157 4158 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4159 4160 else: 4161 infoText += "No data\n" 4162 4163 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4165 def OverviewAccounts(self, show: bool = False) -> dict: 4166 """ 4167 Method for parsing and show simple table with all available user accounts. 4168 4169 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4170 4171 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4172 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4173 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4174 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4175 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4176 "closed": "—", "access": "Full access" }, ...}}` 4177 """ 4178 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4179 4180 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4181 accounts = { 4182 item["id"]: { 4183 "type": TKS_ACCOUNT_TYPES[item["type"]], 4184 "name": item["name"], 4185 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4186 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4187 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4188 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4189 } for item in rawAccounts["accounts"] 4190 } 4191 4192 # Raw and parsed data with some fields replaced in "stat" section: 4193 view = { 4194 "rawAccounts": rawAccounts, 4195 "stat": accounts, 4196 } 4197 4198 # --- Prepare simple text table with only accounts data in human-readable format: 4199 if show: 4200 info = [ 4201 "# User accounts\n\n", 4202 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4203 "| Account ID | Type | Status | Name |\n", 4204 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4205 ] 4206 4207 for account in view["stat"].keys(): 4208 info.extend([ 4209 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4210 account, 4211 view["stat"][account]["type"], 4212 view["stat"][account]["status"], 4213 view["stat"][account]["name"], 4214 ) 4215 ]) 4216 4217 infoText = "".join(info) 4218 4219 uLogger.info(infoText) 4220 4221 if self.userAccountsFile: 4222 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4223 fH.write(infoText) 4224 4225 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4226 4227 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4229 def OverviewUserInfo(self, show: bool = False) -> dict: 4230 """ 4231 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4232 4233 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4234 4235 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4236 :return: dict with raw parsed data from server and some calculated statistics about it. 4237 """ 4238 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4239 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4240 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4241 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4242 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4243 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4244 4245 # This is dict with parsed common user data: 4246 userInfo = { 4247 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4248 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4249 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4250 "tariff": rawUserInfo["tariff"], 4251 } 4252 4253 # This is an array of dict with parsed margin statuses for every account IDs: 4254 margins = {} 4255 for accountId in accounts.keys(): 4256 if rawMargins[accountId]: 4257 margins[accountId] = { 4258 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4259 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4260 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4261 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4262 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4263 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4264 } 4265 4266 else: 4267 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4268 4269 unary = {} # unary-connection limits 4270 for item in rawTariffLimits["unaryLimits"]: 4271 if item["limitPerMinute"] in unary.keys(): 4272 unary[item["limitPerMinute"]].extend(item["methods"]) 4273 4274 else: 4275 unary[item["limitPerMinute"]] = item["methods"] 4276 4277 stream = {} # stream-connection limits 4278 for item in rawTariffLimits["streamLimits"]: 4279 if item["limit"] in stream.keys(): 4280 stream[item["limit"]].extend(item["streams"]) 4281 4282 else: 4283 stream[item["limit"]] = item["streams"] 4284 4285 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4286 limits = { 4287 "unary": unary, 4288 "stream": stream, 4289 } 4290 4291 # Raw and parsed data as an output result: 4292 view = { 4293 "rawUserInfo": rawUserInfo, 4294 "rawAccounts": rawAccounts, 4295 "rawMargins": rawMargins, 4296 "rawTariffLimits": rawTariffLimits, 4297 "stat": { 4298 "userInfo": userInfo, 4299 "accounts": accounts, 4300 "margins": margins, 4301 "limits": limits, 4302 }, 4303 } 4304 4305 # --- Prepare text table with user information in human-readable format: 4306 if show: 4307 info = [ 4308 "# Full user information\n\n", 4309 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4310 "## Common information\n\n", 4311 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4312 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4313 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4314 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4315 "\n## User accounts\n\n", 4316 ] 4317 4318 for account in view["stat"]["accounts"].keys(): 4319 info.extend([ 4320 "### ID: [{}]\n\n".format(account), 4321 "| Parameters | Values |\n", 4322 "|----------------------|--------------------------------------------------------------|\n", 4323 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4324 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4325 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4326 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4327 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4328 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4329 ]) 4330 4331 if margins[account]: 4332 info.extend([ 4333 "| Margin status: | Enabled |\n", 4334 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4335 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4336 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4337 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4338 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4339 ]) 4340 4341 else: 4342 info.append("| Margin status: | Disabled |\n\n") 4343 4344 info.extend([ 4345 "\n## Current user tariff limits\n", 4346 "\nSee also:\n", 4347 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4348 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4349 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4350 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4351 "\n### Unary limits\n", 4352 ]) 4353 4354 if unary: 4355 for key, values in sorted(unary.items()): 4356 info.append("\n* Max requests per minute: {}\n".format(key)) 4357 4358 for value in values: 4359 info.append(" - {}\n".format(value)) 4360 4361 else: 4362 info.append("\nNot available\n") 4363 4364 info.append("\n### Stream limits\n") 4365 4366 if stream: 4367 for key, values in sorted(stream.items()): 4368 info.append("\n* Max stream connections: {}\n".format(key)) 4369 4370 for value in values: 4371 info.append(" - {}\n".format(value)) 4372 4373 else: 4374 info.append("\nNot available\n") 4375 4376 infoText = "".join(info) 4377 4378 uLogger.info(infoText) 4379 4380 if self.userInfoFile: 4381 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4382 fH.write(infoText) 4383 4384 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4385 4386 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4389class Args: 4390 """ 4391 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4392 """ 4393 def __init__(self, **kwargs): 4394 self.__dict__.update(kwargs) 4395 4396 def __getattr__(self, item): 4397 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4400def ParseArgs(): 4401 """This function get and parse command line keys.""" 4402 parser = ArgumentParser() # command-line string parser 4403 4404 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4405 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4406 4407 # --- options: 4408 4409 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4410 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4411 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4412 4413 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4414 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4415 4416 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4417 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4418 4419 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4420 4421 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4422 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4423 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4424 4425 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4426 4427 # --- commands: 4428 4429 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4430 4431 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4432 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4433 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4434 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4435 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4436 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4437 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4438 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4439 4440 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4441 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4442 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4443 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4444 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4445 4446 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4447 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4448 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4449 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4450 4451 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4452 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4453 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4454 4455 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4456 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4457 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4458 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4459 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4460 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4461 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4462 4463 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4464 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4465 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4466 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4467 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4468 4469 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4470 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4471 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4472 4473 cmdArgs = parser.parse_args() 4474 return cmdArgs
This function get and parse command line keys.
4477def Main(**kwargs): 4478 """ 4479 Main function for work with TKSBrokerAPI in the console. 4480 4481 See examples: 4482 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4483 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4484 """ 4485 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4486 4487 if args.debug_level: 4488 uLogger.level = 10 # always debug level by default 4489 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4490 4491 exitCode = 0 4492 start = datetime.now(tzutc()) 4493 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4494 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4495 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4496 )) 4497 4498 # trying to calculate full current version: 4499 buildVersion = __version__ 4500 try: 4501 v = version("tksbrokerapi") 4502 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4503 4504 except Exception: 4505 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4506 4507 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4508 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4509 4510 try: 4511 if args.version: 4512 print("TKSBrokerAPI {}".format(buildVersion)) 4513 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4514 4515 else: 4516 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4517 server = TinkoffBrokerServer( 4518 token=args.token, 4519 accountId=args.account_id, 4520 useCache=not args.no_cache, 4521 ) 4522 4523 # --- set some options: 4524 4525 if args.ticker: 4526 if args.ticker in server.aliasesKeys: 4527 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4528 4529 else: 4530 server.ticker = args.ticker 4531 4532 if args.figi: 4533 server.figi = args.figi 4534 4535 if args.depth is not None: 4536 server.depth = args.depth 4537 4538 # --- do one of commands: 4539 4540 if args.list: 4541 if args.output is not None: 4542 server.instrumentsFile = args.output 4543 4544 server.ShowInstrumentsInfo(show=True) 4545 4546 elif args.list_xlsx: 4547 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4548 4549 elif args.bonds_xlsx is not None: 4550 if args.output is not None: 4551 server.bondsXLSXFile = args.output 4552 4553 if len(args.bonds_xlsx) == 0: 4554 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4555 4556 else: 4557 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4558 4559 elif args.search: 4560 if args.output is not None: 4561 server.searchResultsFile = args.output 4562 4563 server.SearchInstruments(pattern=args.search[0], show=True) 4564 4565 elif args.info: 4566 if not (args.ticker or args.figi): 4567 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4568 raise Exception("Ticker or FIGI required") 4569 4570 if args.output is not None: 4571 server.infoFile = args.output 4572 4573 if args.ticker: 4574 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4575 4576 else: 4577 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4578 4579 elif args.calendar is not None: 4580 if args.output is not None: 4581 server.calendarFile = args.output 4582 4583 if len(args.calendar) == 0: 4584 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4585 4586 else: 4587 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4588 4589 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4590 4591 elif args.price: 4592 if not (args.ticker or args.figi): 4593 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4594 raise Exception("Ticker or FIGI required") 4595 4596 server.GetCurrentPrices(show=True) 4597 4598 elif args.prices is not None: 4599 if args.output is not None: 4600 server.pricesFile = args.output 4601 4602 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4603 4604 elif args.overview: 4605 if args.output is not None: 4606 server.overviewFile = args.output 4607 4608 server.Overview(show=True, details="full") 4609 4610 elif args.overview_digest: 4611 if args.output is not None: 4612 server.overviewDigestFile = args.output 4613 4614 server.Overview(show=True, details="digest") 4615 4616 elif args.overview_positions: 4617 if args.output is not None: 4618 server.overviewPositionsFile = args.output 4619 4620 server.Overview(show=True, details="positions") 4621 4622 elif args.overview_orders: 4623 if args.output is not None: 4624 server.overviewOrdersFile = args.output 4625 4626 server.Overview(show=True, details="orders") 4627 4628 elif args.overview_analytics: 4629 if args.output is not None: 4630 server.overviewAnalyticsFile = args.output 4631 4632 server.Overview(show=True, details="analytics") 4633 4634 elif args.deals is not None: 4635 if args.output is not None: 4636 server.reportFile = args.output 4637 4638 if 0 <= len(args.deals) < 3: 4639 server.Deals( 4640 start=args.deals[0] if len(args.deals) >= 1 else None, 4641 end=args.deals[1] if len(args.deals) == 2 else None, 4642 show=True, # Always show deals report in console 4643 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4644 ) 4645 4646 else: 4647 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4648 raise Exception("Incorrect value") 4649 4650 elif args.history is not None: 4651 if args.output is not None: 4652 server.historyFile = args.output 4653 4654 if 0 <= len(args.history) < 3: 4655 dataReceived = server.History( 4656 start=args.history[0] if len(args.history) >= 1 else None, 4657 end=args.history[1] if len(args.history) == 2 else None, 4658 interval="hour" if args.interval is None or not args.interval else args.interval, 4659 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4660 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4661 show=True, # shows all downloaded candles in console 4662 ) 4663 4664 if args.render_chart is not None and dataReceived is not None: 4665 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4666 4667 server.ShowHistoryChart( 4668 candles=dataReceived, 4669 interact=iChart, 4670 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4671 ) 4672 4673 else: 4674 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4675 raise Exception("Incorrect value") 4676 4677 elif args.load_history is not None: 4678 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4679 4680 if args.render_chart is not None and histData is not None: 4681 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4682 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4683 4684 server.ShowHistoryChart( 4685 candles=histData, 4686 interact=iChart, 4687 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4688 ) 4689 4690 elif args.trade is not None: 4691 if 1 <= len(args.trade) <= 5: 4692 server.Trade( 4693 operation=args.trade[0], 4694 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4695 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4696 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4697 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4698 ) 4699 4700 else: 4701 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4702 4703 elif args.buy is not None: 4704 if 0 <= len(args.buy) <= 4: 4705 server.Buy( 4706 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4707 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4708 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4709 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4710 ) 4711 4712 else: 4713 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4714 4715 elif args.sell is not None: 4716 if 0 <= len(args.sell) <= 4: 4717 server.Sell( 4718 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4719 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4720 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4721 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4722 ) 4723 4724 else: 4725 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4726 4727 elif args.order: 4728 if 4 <= len(args.order) <= 7: 4729 server.Order( 4730 operation=args.order[0], 4731 orderType=args.order[1], 4732 lots=int(args.order[2]), 4733 targetPrice=float(args.order[3]), 4734 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4735 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4736 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4737 ) 4738 4739 else: 4740 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4741 4742 elif args.buy_limit: 4743 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4744 4745 elif args.sell_limit: 4746 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4747 4748 elif args.buy_stop: 4749 if 2 <= len(args.buy_stop) <= 7: 4750 server.BuyStop( 4751 lots=int(args.buy_stop[0]), 4752 targetPrice=float(args.buy_stop[1]), 4753 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4754 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4755 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4756 ) 4757 4758 else: 4759 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4760 4761 elif args.sell_stop: 4762 if 2 <= len(args.sell_stop) <= 7: 4763 server.SellStop( 4764 lots=int(args.sell_stop[0]), 4765 targetPrice=float(args.sell_stop[1]), 4766 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4767 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4768 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4769 ) 4770 4771 else: 4772 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4773 4774 # elif args.buy_order_grid is not None: 4775 # # update order grid work with api v2 4776 # if len(args.buy_order_grid) == 2: 4777 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4778 # 4779 # for order in orderParams: 4780 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4781 # 4782 # else: 4783 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4784 # 4785 # elif args.sell_order_grid is not None: 4786 # # update order grid work with api v2 4787 # if len(args.sell_order_grid) >= 2: 4788 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4789 # 4790 # for order in orderParams: 4791 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4792 # 4793 # else: 4794 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4795 4796 elif args.close_order is not None: 4797 server.CloseOrders(args.close_order) # close only one order 4798 4799 elif args.close_orders is not None: 4800 server.CloseOrders(args.close_orders) # close list of orders 4801 4802 elif args.close_trade: 4803 if not args.ticker: 4804 uLogger.error("`--ticker` key is required for this operation!") 4805 raise Exception("Ticker required") 4806 4807 server.CloseTrades([args.ticker]) # close only one trade 4808 4809 elif args.close_trades is not None: 4810 server.CloseTrades(args.close_trades) # close trades for list of tickers 4811 4812 elif args.close_all is not None: 4813 server.CloseAll(*args.close_all) 4814 4815 elif args.limits: 4816 if args.output is not None: 4817 server.withdrawalLimitsFile = args.output 4818 4819 server.OverviewLimits(show=True) 4820 4821 elif args.user_info: 4822 if args.output is not None: 4823 server.userInfoFile = args.output 4824 4825 server.OverviewUserInfo(show=True) 4826 4827 elif args.account: 4828 if args.output is not None: 4829 server.userAccountsFile = args.output 4830 4831 server.OverviewAccounts(show=True) 4832 4833 else: 4834 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4835 raise Exception("There is no command to execute") 4836 4837 except Exception: 4838 trace = tb.format_exc() 4839 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4840 if e in trace: 4841 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4842 break 4843 4844 uLogger.debug(trace) 4845 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4846 exitCode = 255 # an error occurred, must be open a ticket for this issue 4847 4848 finally: 4849 finish = datetime.now(tzutc()) 4850 4851 if exitCode == 0: 4852 uLogger.debug("All operations were finished success (summary code is 0).") 4853 4854 else: 4855 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4856 os.path.abspath(uLog.defaultLogFile), exitCode, 4857 )) 4858 4859 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4860 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4861 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4862 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4863 )) 4864 4865 if not kwargs: 4866 sys.exit(exitCode) 4867 4868 else: 4869 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: